{"owner":"tw93","repo":"Mole","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md"],"files":{"AGENTS.md":"# Mole Agent Guide\n\nThis file is the shared source of truth for any AI agent working on this repo (Claude Code, Codex, etc.). `CLAUDE.md` is a symlink to this file. Put machine-specific or personal overrides in `AGENTS.local.md` / `CLAUDE.local.md`; both are gitignored.\n\n## Project\n\nMole is a macOS system cleanup and optimization tool with shell and Go components. It performs file cleanup, app protection checks, and maintenance tasks, so safety rules matter more than speed.\n\n## Product Direction\n\nMole is a terminal-first macOS maintenance toolkit. Its core job is to help power users inspect reclaimable space, remove known-safe leftovers, uninstall apps safely, run bounded maintenance, and check health from a CLI, script, or compact TUI. It is not a general Mac control center, package manager, background monitor, or GUI feature mirror.\n\n### What Mole Should Do\n\n- Make cleanup and uninstall actions boring, reviewable, logged, protected by path/app rules, and dry-run capable.\n- Prefer reversible user-facing removals through Trash where the command surface expects recoverability.\n- Keep `clean`, `uninstall`, `purge`, and `installer` focused on reclaimable files, app leftovers, rebuildable caches, installer artifacts, and exact known cleanup targets.\n- Keep `analyze` as a disk explorer and ad hoc cleanup surface. Optimize first paint, navigation, sorting, filtering, and safe deletion before adding dashboard-style features.\n- Keep `status` as a compact read-only health dashboard plus stable JSON/NDJSON automation output. It may surface actionable signals, but should not become an iStat clone, alerting daemon, or configurable metrics workbench.\n- Keep `optimize` focused on explicit, bounded maintenance tasks that can be explained before execution and tested without real authorization prompts.\n- Keep command UX dense and terminal-native: short labels, stable alignment, predictable shortcuts, one-screen summaries, then optional drill-down.\n- Keep Mole Mac references as a cross-link or support path. The CLI and Mac app can share product values without requiring feature parity.\n\n### What Mole Should Not Do\n\n- Do not add broad system modification, privacy reset, package management, app bundle patching, or device-management features just because they are technically possible.\n- Do not remove or rewrite third-party app bundle contents, signed resources, user documents, credentials, sessions, active databases, or active developer-tool state.\n- Do not add background agents, persistent monitoring, notifications, schedulers, menu bar behavior, or GUI-like state unless explicitly requested and justified as CLI scope.\n- Do not broaden leftover matching from exact app or bundle evidence into vendor-wide, TeamID-prefix, generic-name, or fallback wildcard deletion.\n- Do not turn `status` into a noisy dashboard. Extra rows, live alerts, and tuning controls need a common user action, not just an available metric.\n- Do not add prompts, preferences, or output modes to solve every edge case. Prefer quieter defaults, preview/read-only guidance, or declining unsupported operations. A new flag, environment variable, or config key is the same weight as a new setting: it passes only when no single default is right for everyone, and the fix-by-default alternative has to be stated and rejected first. Reaching for a knob to close an issue is the default failure here, not an edge case.\n- Do not treat Mole Mac features as required CLI gaps. The CLI should stay narrower, scriptable, and safety-first when parity would add complexity or ambiguity.\n\n### Product Decision Filter\n\nBefore accepting a new feature, answer these questions in the PR, issue, or review notes when the fit is not obvious:\n\n1. Does it clearly belong to clean, uninstall, analyze, optimize, status, purge, history, installer, update, completion, touchid, or remove?\n2. Is it safe by default, previewable where destructive, testable without real auth, and explainable in one terminal screen?\n3. Can the user verify what will change before Mole changes it?\n4. Is the target data locally rebuildable, disposable, or backed by exact app/bundle evidence?\n5. Would this be better as Mole Mac UI, documentation, a warning, or an explicit \"not supported\" answer?\n\nIf the answer is no or unclear, decline the feature, narrow it, or park it until the product value beats the added surface area.\n\n## Repository Map\n\n- `AGENTS.md` is the cross-agent source of truth. `CLAUDE.md` must remain a symlink to it so Claude and Codex receive the same project contract.\n- `.claude/skills/` is the canonical home for project skills. `.agents/skills/` contains relative symlinks for Codex discovery; do not maintain copied skill bodies.\n- `.claude/agents/` contains focused Claude review profiles. They must read the current contract from this file instead of copying a frozen version of the safety or portability rules.\n- `mole` - the CLI entrypoint. It is a **router only**: it parses args, renders the menu, and dispatches. Business logic does not belong here. Self-update lives in `lib/manage/update.sh` and self-removal in `lib/manage/remove.sh`; both are `source`d (not `exec`d) because the interactive menu and the update banner call them in-process. `VERSION=` stays in `mole` because `install.sh` reads it out of this file with `sed`.\n- `lib/core/` - shared shell safety, UI, file operations, operation logs, app protection logic, and centralized timeout constants (`timeouts.sh`).\n- `lib/core/app_protection_data.sh` - readonly bundle ID and pattern arrays consumed by `app_protection.sh`. Data only, no logic.\n- `cmd/analyze/` - Go disk-analysis TUI. `main.go` is bootstrap only; `model.go` holds types and accessor methods; `update.go` holds the Bubble Tea Update chain.\n- `tests/fuzz_corpus/` holds property-test corpora consumed by `path_validation_fuzz.bats`.\n- `scripts/` - check, test, build, and release helpers. `audit_bundle_drift.sh` backs the monthly bundle audit; `audit_function_duplication.py` gates same-body-different-name shell functions and runs inside `check.sh` (`--list` shows every group); per-PR perf is covered by `tests/core_performance.bats`.\n- `docs/SECURITY_DESIGN.md` - design doc for the path validation / app protection / # SAFE annotation contract.\n- `SECURITY_AUDIT.md` - security review notes.\n\n## Commands\n\n```bash\n./scripts/check.sh --format\nMOLE_TEST_NO_AUTH=1 ./scripts/test.sh\nMOLE_TEST_NO_AUTH=1 bats tests/clean_core.bats\nMOLE_DRY_RUN=1 ./mole clean\nMOLE_TEST_NO_AUTH=1 ./mole clean --dry-run\nMOLE_TEST_NO_AUTH=1 ./mole purge --dry-run\nMOLE_TEST_NO_AUTH=1 ./mole installer --dry-run\nfind bin lib -name '*.sh' -print0 | xargs -0 -n1 bash -n\nmake build\ngo test ./...\n```\n\nPublic docs and examples should prefer the installed `mo` command. Use `./mole` in this repository when verifying source-tree behavior before installation. `analyze` and `analyse` are both accepted command spellings.\n\n## Critical Safety Rules\n\n- Route deletion through the safe helpers in `lib/core/file_ops.sh`. Raw `rm -rf` and `find -delete` are allowed only with a `# SAFE: <one-sentence reason>` annotation on the same line, which is the contract `docs/SECURITY_DESIGN.md` Layer 2 defines and `.github/workflows/test.yml` enforces by whitelist; seven `rm -rf` call sites use it today for paths the function itself created. Grepping `# SAFE:` returns far more than seven because ~100 `rm -f` removals of self-created mktemp files carry the same annotation, which is the prescribed pattern, not drift: the CI whitelist checks that the annotation is present and counts nothing. Do not route a mktemp scratch path through `mole_delete`: that adds Trash routing and an operation-log entry to a temp file.\n- Use `mole_delete` from `lib/core/file_ops.sh` for removals so Trash routing, operation logs, dry-run behavior, and path protection stay consistent.\n- Never modify protected paths such as `/System`, `/Library/Apple`, or `com.apple.*`.\n- Route user-facing cleanup through Trash where the project expects recoverability, especially for analyze-driven ad hoc cleanup.\n- Never let verification block on sudo, AppleScript, or macOS authorization prompts unless the task explicitly targets auth behavior.\n- Use `MOLE_DRY_RUN=1` before destructive cleanup flows.\n- Use `MOLE_TEST_NO_AUTH=1` for tests, manual repro, and verification unless real auth behavior is being tested.\n- Any new direct use of `sudo`, `osascript`, or `launchctl` must have a `MOLE_TEST_MODE` / `MOLE_TEST_NO_AUTH` guard or be fully mocked in tests.\n- Never auto-delete Software Update-owned staging trees such as `/Library/Updates` or `/macOS Install Data`. Directory age, process lists, and Software Update plist state cannot prove those trees stay inactive across a scan-to-delete window; keep this surface read-only.\n- Never delete, truncate, or vacuum the active PowerLog database at `/private/var/db/powerlog/Library/PerfPowerTelemetry/BackgroundProcessing/CurrentBackgroundProcessingDB.BGSQL` or its `-wal` / `-shm` companions. Size and mtime cannot prove that Apple has closed every SQLite connection; keep abnormal-size handling read-only.\n- Never run a privileged path-based delete or move through an invoking-user-mutable ancestor. `safe_sudo_remove`, `safe_sudo_find_delete`, and `mole_delete` must downgrade or fail closed there; privileged Trash moves must cross into dedicated immutable root-owned staging under `/Library` before the invoking user moves the item into Trash.\n- **`install.sh` stays fail-closed on verification failure.** A checksum or attestation mismatch aborts and says why; it must never downgrade to a source build, which turns \"the binary was tampered with\" into a quieter path with weaker verification. Resolving no release tag and falling back to `main` must warn that this is a nightly source install. The abort cases in `tests/install_checksum.bats` pin both. Keep the README install URL on unpinned `main`: pinning it there blocks fixes from reaching new installs.\n- **A gate that refuses must name which cause it hit and what to run next.** Install and update gates fail for causes with nothing in common: an untrusted ancestor, no admin session, a planted lock path, real contention. \"Reinstall\" fixes none of them, so a single catch-all message leaves the user with no move. `acquire_install_lock` returns a reason through `INSTALL_LOCK_FAILURE` and `report_install_lock_failure` prints one line of cause plus one line of command; keep that shape and add a reason rather than widening the catch-all. Two traps this area has already sprung: a new gate placed in front of an older, better-diagnosed failure silently downgrades the diagnosis, so when adding one, check what the old path used to say and keep it at least as actionable; and never pin a catch-all string in a source-invariant test, which is how the fix for #1335 swapped one vague message for another and locked it in as a requirement. Pin the reason codes. A source-invariant test that greps for a forbidden call must strip comment lines first: the comment explaining why `brew list mole` was removed read as the call coming back, and the guard failed on prose rather than on code. One cause can carry several: `install_lock_has_unsafe_ancestor` refuses for a symlink, an unreadable stat, a foreign owner, a loose mode, or an ACL, and they need different commands, since `chown` does not clear an ACL and `chmod` does not undo a symlink. It reports which through `INSTALL_LOCK_UNSAFE_ANCESTOR_REASON`; `tests/install_checksum.bats` asserts every raised code reaches its own branch and every branch names a next step.\n- The `mo update` self-heal fallback (`_update_self_heal_reinstall`) exists because the local bootstrap (temp file, registry, exec) is frozen on the user's machine and a broken installed version cannot fix itself (#1297). Keep it streaming install.sh from `main` straight into bash with no local temp files. Stable success is asserted against the installed binary's bounded version response, never installer output (the V1.47.1 false-success shape), and `install.sh` must bound its own `--version` / `--help` verification probes too. Nightly success additionally requires a per-attempt install receipt; pin the source archive to the resolved commit when HEAD is known, and never reuse an older `COMMIT_HASH` when it is not. Keep updates single-flight per install directory so receipt, commit metadata, and binary verification cannot cross concurrent generations: both writers take the same target-adjacent mutex, preferring absolute `/usr/bin/lockf` because the kernel drops that lock even if the holder is killed. `lockf` only ships with newer macOS, so requiring it made install and update exit before writing a file on every older release (#1348); where it is absent both fall back to an atomic `mkdir` in the lock directory, reclaiming it only against proof the recorded owner is gone (dead pid, or a live pid whose start time no longer matches). Distinguish the two fail-closed cases: a lock command that *runs and refuses* is contention, a platform that never had one is not, and only a system with neither primitive is turned away. Do not build the wrapper as a shell array; the empty one is the fallback path and an empty array under `set -u` is an unbound-variable error on the bash 3.2 macOS ships. Regression tests live in `tests/update.bats` and `tests/install_checksum.bats`.\n- Do not change ESC timeout behavior in `lib/core/ui.sh` unless explicitly requested.\n- Preserve operation logging to the project log path unless the user explicitly asks to change `MO_NO_OPLOG` behavior.\n- **PRs touching destructive sinks need line-by-line review.** For `find_app_files`, `mole_delete`, `remove_file_list`, container traversal, identifier-prefix wildcards, or recursion that ends in deletion, audit every primary and fallback branch for matcher breadth, protected-path coverage, and preserved confirmation. Exact bundle ID or path evidence is required; vendor prefixes and common-name globs are not. Treat specialist or AI review output as a claim to verify, never as approval.\n\n## Working Rules\n\n- Before reviewing, auditing, debugging, or accepting a contributed PR, read `.claude/skills/bugs/SKILL.md`. Load its shell/test reference only when that surface is touched.\n- Check `should_protect_path()` before adding cleanup behavior.\n- Check app protection helpers before adding app cache, uninstall, or leftover cleanup behavior.\n- Bundle protection matching is case-sensitive glob (`bundle_matches_pattern`), and macOS system bundles report inconsistent casing across releases (macOS 26 ships `com.apple.bootcampassistant` alongside the older `com.apple.BootCampAssistant`). When the monthly bundle drift audit reports gaps, add the exact IDs as the audit printed them, and check the runtime blanket `com.apple.*` guard before rating the gap's severity. The audit workflow's issue path requires the `bundle-drift` label to exist in the repo.\n- **A new cleanup target needs measured value and an explicit non-target list.** State bytes reclaimed on a real app version, which sibling directories are excluded because they are user data, and whether protection covers every reachable cleanup path. \"It looks like a cache\" is not evidence; zero measured value stays out of scope. An encrypted or opaque index cannot prove a directory is unreferenced, so exclude it.\n- Keep AI-tool cache cleanup conservative. Claude Code, opencode, Copilot CLI, Zed, Warp, Ghostty, and similar developer tools may have active versions, config, credentials, or session state that must not be removed accidentally.\n- Do not clean tiny macOS UI state just because it is rebuildable. Wallpaper previews, preference thumbnails, and similar cover/state caches can create visible blank or cloud-download UI while reclaiming only a few MB; keep them unless there is strong user value and a regression test.\n- Homebrew cleanup must be preview-first. Show the exact `brew autoremove` candidates before removal, preserve dry-run behavior, and keep tests on mocked `brew`; do not let a cleanup path execute real package-manager removals in verification.\n- Sudo gates must not treat typed password characters as \"skip\". Only an explicit skip key should skip privileged cleanup; direct typed input must proceed into the real sudo prompt and have a regression test.\n- Long cleanup scans need both an overall wall-clock budget and inner-loop checkpoints. A timed-out producer must not feed partial output into a deletion loop: materialize only completed scans, discard results on nonzero status, and propagate timeout/failure instead of reporting success. Probe and action must use the same pattern, type, age, and depth. If a project/artifact scan times out, degrade to partial or skipped-slow-scan output instead of appearing hung.\n- System-service orphan scans must parse plist `Program` / `ProgramArguments` values as absolute paths only. Use non-interactive sudo for unreadable root-owned plists when needed, reject PlistBuddy error text as data, and keep CI tests on `/Library/LaunchDaemons` rather than relying on `/Library/PrivilegedHelperTools`.\n- Uninstall leftover expansion must stay exact and boring: bundle ID or app-name variants only, reject generic/common words, keep short-name floors, skip broad locations like `Preferences/ByHost`, and only remove helper remnants after the parent app is confirmed gone and protected-path checks pass.\n- Any new uninstall teardown path (launch services, login items, cask zap, helper bootout) must route through the shared-bundle-id sibling guard, covering `/Volumes` copies, inverse-name, and shared-identity variants, with a Bats regression per variant.\n- Preference repair and optimize cleanup must skip protected and whitelisted plists before attempting removal.\n- **Git worktree staleness is not decidable.** Clean only whitelisted rebuildable artifacts inside a worktree, never the worktree itself, and never emit a \"safe to delete\" verdict. Branch/remote heuristics fail on detached worktrees, ordinary status hides ignored files, and ignored entries may be the only copy of private state. A status surface may report blockers only: dirty, unpushed, locked, or ignored entries outside `MOLE_PURGE_TARGETS`.\n- Purge discovery skips dot-directory containers by design. Add each supported container, such as `~/.codex/worktrees`, explicitly to `MOLE_PURGE_DEFAULT_SEARCH_PATHS`; do not broaden discovery to all dot directories. The scan layer already handles hidden descendants once their parent container is known, while project roots deeper than the existing two-level probe remain intentionally out of scope.\n- **Do not add a shell-side directory size cache.** APFS does not propagate mtime up the tree, so a parent directory's mtime is unchanged when a descendant grows or shrinks and the cache hands the user a stale reclaimable number. Measure every time; `get_path_size_kb` is already timeout-bounded.\n- Keep shell code formatted with `./scripts/check.sh --format`.\n- Prefer targeted Bats tests during development; run the full suite before committing.\n- Do not add AI attribution trailers to commits.\n- `start_section` / `end_section` / `note_activity` have three intentionally different implementations in `lib/core/base.sh`, `bin/clean.sh`, and `bin/purge.sh`. Source order decides which one wins, and the wording, color, and dry-run export semantics differ on purpose. Read the cross-reference comment in `lib/core/base.sh` before changing any of them.\n- **Judge duplication by body, not by name.** Copies that matter have already been renamed, so grep and a read-through both class them as separate helpers: two guards differing only by a `_MOLE_DEV_` vs `_MOLE_USER_` prefix, nine more re-implementing one process-state translation. `scripts/audit_function_duplication.py` hashes normalized bodies and gates on new groups; it found two pairs a manual sweep of the same release had missed. It is blind in the other direction: a probe plus guard that duplicate a live pair's purpose while differing in both name and body pass it cleanly, which is how a second, narrower Autodesk guard sat unreferenced until a caller sweep found it. The same caution applies to counts quoted in this file: check what a number counts before calling it stale, since `# SAFE:` legitimately annotates ~100 `rm -f` lines while its \"seven\" refers only to `rm -rf` under the CI-checked `lib/ bin/ install.sh mole` scope.\n- **Test-orphan pattern:** before declaring a symbol dead, grep `lib`, `bin`, `cmd`, `scripts`, `tests`, and top-level entry/install scripts; check dynamic lookup through `eval`, `declare -f`, and `compgen`; then re-grep after removal. Trace variables and config written by a removed helper. Tests alone are not production callers, and sub-agent reports are leads, not verdicts.\n- **`mole_clean_process_guard` in `lib/core/base.sh` is the only translator of the probe tri-state** (`0` running, `1` not, `2` could not tell). State `2` denies; a copy that folds it into \"not running\" deletes a live app's files while every other copy still reads correctly in review. Compound guards call it for the process question and add their own evidence after; eligibility goes through `mole_cleanup_targets_exist` (predicate list must match `_safe_clean_impl`'s), refusals through `mole_report_guard_stop`. Locked by `mole_clean_process_guard denies on an unknown process state` and `cleanup delete guards do not re-implement the process-state translation`. Left open-coded on purpose: the scan-stage `state -eq 2` blocks pick per-section wording, and the Codex open-file probe inverts the contract.\n- **A `declare -f` probe into `bin/` is a shared shim, never a per-file copy.** Asking whether `safe_clean_guarded` or `defer_cleanup_family` exists is asking whether `bin/clean.sh` is loaded: always in production, never in a standalone Bats case. So each branch is a degraded second copy of a delete decision that only tests run. Use `mole_defer_cleanup_family`, or call `safe_clean_guarded` directly and let the test supply it. Locked by `cleanup libs share one engine-absent shim instead of forking their own` and `engine-absent cleanup fallbacks stay at their audited count`; lowering that cap after a removal is expected, raising it needs a reason. Rejected: hoisting `_safe_clean_impl` into `lib/core/` to delete the fallbacks, which bypasses the `safe_clean` stub 235 test points rely on and turns stubbed assertions into real deletions under a temp `HOME`.\n\n## Hotspot Ownership\n\nThese files are intentionally large. Do not start by splitting them. Keep edits narrow, preserve local safety boundaries, and run the listed tests when touching each area.\n\n- `lib/clean/user.sh` owns user-level cleanup flows, browser caches, cloud/app support cleanup, device firmware, and Apple Silicon caches. Run `MOLE_TEST_NO_AUTH=1 bats tests/clean_user_core.bats tests/clean_browser_versions.bats tests/clean_app_caches.bats tests/clean_cached_device_firmware.bats` when touching this area, or `MOLE_TEST_NO_AUTH=1 ./scripts/test.sh` if behavior crosses sections. Chrome / Edge / Brave old-version cleanup is one table-driven helper (`_clean_chromium_old_versions`) plus three thin public wrappers; the wrapper names are the test surface, so keep them. `clean_edge_updater_old_versions` is deliberately NOT part of it: it prunes staged updater payloads strictly older than the installed Edge (falling back to keep-latest by `sort -V` when the installed version is unreadable), has no `Current` symlink, and never escalates to a sudo removal, so folding it in would silently change its semantics.\n- `lib/core/app_protection.sh` owns uninstall/data/path protection policy and bundle matching; `lib/core/app_protection_data.sh` owns the protected app category lists. Run `MOLE_TEST_NO_AUTH=1 bats tests/uninstall_safety.bats tests/uninstall_naming_variants.bats tests/bundle_resolver.bats`.\n- `lib/clean/project.sh` owns purge discovery, project artifact filtering, purge menus, and purge config. Run `MOLE_TEST_NO_AUTH=1 bats tests/purge.bats tests/purge_config_paths.bats`.\n- `bin/uninstall.sh` owns uninstall command orchestration, app inventory, metadata refresh, and list/json output. Run `MOLE_TEST_NO_AUTH=1 bats tests/uninstall.bats tests/uninstall_scan_bash32.bats`.\n- `lib/uninstall/batch.sh` owns batch uninstall execution, the shared-bundle-id sibling guard, launch service and login item teardown, and brew cask removal routing. Run `MOLE_TEST_NO_AUTH=1 bats tests/uninstall.bats tests/brew_uninstall.bats tests/uninstall_remove_file_list.bats`.\n- `lib/clean/dev.sh` owns developer-tool cleanup, language/toolchain caches, AI agent caches, and Codex runtime handling. Run `MOLE_TEST_NO_AUTH=1 bats tests/clean_dev_caches.bats tests/dev_extended.bats`.\n- `lib/optimize/tasks.sh` owns optimize task registration and system maintenance actions. Run `MOLE_TEST_NO_AUTH=1 bats tests/optimize.bats tests/optimize_db.bats`.\n- `bin/clean.sh` owns clean command orchestration, section output, and safe cleanup execution. Run `MOLE_TEST_NO_AUTH=1 bats tests/clean_core.bats tests/clean_apps.bats tests/cli.bats`. Section output follows one fixed rhythm: title → loading state → content → one trailing blank line, for every section. When touching any step of it, re-run the command and read the whole rendered output (column alignment, block spacing, icon consistency) instead of patching the one step that was reported. `_safe_clean_impl` filters protected, whitelisted, compiled-cache, and missing targets before consulting the dry-run delete guard or registering previews, so preview and real cleanup use the same eligible set; that check is intentional, not redundant with the per-path checks the real branch keeps at its deletion boundary.\n- `lib/manage/update.sh` owns self-update, registry/bootstrap replacement, and self-heal fallback behavior. Preserve fail-closed version checks and test both normal update and broken-bootstrap recovery with `MOLE_TEST_NO_AUTH=1 bats tests/update.bats`.\n- `cmd/analyze/update.go` owns the Bubble Tea `Update` chain and message handlers (Init, scanCmd, updateKey, goBack, switchToOverviewMode, enterSelectedDir). This is the largest file in `cmd/analyze/` and the natural landing spot for new key bindings, message types, or navigation behavior. Run `go test ./cmd/analyze`. `cmd/analyze/main.go` is bootstrap only (flag parsing, `main()`, helpers); `cmd/analyze/model.go` holds types and the model struct.\n- `cmd/analyze/cache.go` owns analyze cache schema, expiry, load/save, invalidation, and cacheability decisions. Computation changes must invalidate stale persisted data in the same change. Run `go test ./cmd/analyze`.\n- `cmd/analyze/analyze_test.go` and `cmd/status/view_test.go` are test hotspots. Add new cases near related behavior; split later only when touching many adjacent cases. Run `go test ./cmd/...`.\n- `lib/core/file_ops.sh` owns the deletion funnel, Trash/permanent routing, operation-log outcomes, size accounting, and last-mile path validation. `lib/core/base.sh` owns shared shell primitives and source-order-sensitive section helpers. Keep policy in the existing protection helpers rather than adding a second delete path. Run `MOLE_TEST_NO_AUTH=1 bats tests/file_ops_mole_delete.bats tests/file_ops_size.bats tests/file_ops_safe_remove_symlink.bats tests/user_file_ops.bats tests/core_safe_functions.bats`.\n- `cmd/analyze/scanner.go` owns disk traversal, Spotlight integration, cancellation, and all scan concurrency budgets. Treat its semaphores as independent resource limits and measure before changing them. Run `go test ./cmd/analyze`.\n- `lib/clean/apps.sh` owns application-data cleanup, orphan service discovery, and the narrow verified-container-stub exception. `lib/clean/hints.sh` is read-only guidance and must stay bounded, timeout-aware, and non-destructive. Run `MOLE_TEST_NO_AUTH=1 bats tests/clean_apps.bats tests/clean_hints.bats`.\n- `lib/ui/menu_paginated.sh` owns the shared Bash 3.2-compatible selection UI and terminal restoration. Preserve trap chaining, TTY restoration, and empty-selection behavior. Run `MOLE_TEST_NO_AUTH=1 bats tests/menu_trap_restore.bats tests/uninstall.bats`.\n- `cmd/status/view.go` owns status rendering only; collection and JSON/NDJSON contracts live elsewhere in `cmd/status/`. Keep narrow-terminal layout and automation output independent. Run `go test ./cmd/status` and `MOLE_TEST_NO_AUTH=1 bats tests/cli.bats` when command routing changes.\n- `bin/installer.sh` owns installer discovery, immutable delete-plan validation, the paginated selection flow, and incomplete-cleanup exit semantics. Run `MOLE_TEST_NO_AUTH=1 bats tests/installer.bats tests/installer_fd.bats tests/installer_zip.bats`.\n\n## Verification\n\n- Shell changes: run `./scripts/check.sh --format`, then the relevant Bats test or `MOLE_TEST_NO_AUTH=1 ./scripts/test.sh`.\n- Go changes: run `go test ./...`.\n- Cleanup behavior: verify with dry-run or test mode first.\n- File operation changes: run `MOLE_TEST_NO_AUTH=1 bats tests/file_ops_mole_delete.bats tests/user_file_ops.bats`.\n- Installer changes: run `MOLE_TEST_NO_AUTH=1 bats tests/installer.bats tests/installer_fd.bats tests/installer_zip.bats`.\n- Purge changes: run `MOLE_TEST_NO_AUTH=1 bats tests/purge.bats tests/purge_config_paths.bats`.\n- Whitelist or management changes: run `MOLE_TEST_NO_AUTH=1 bats tests/manage_whitelist.bats tests/manage_sudo.bats`.\n- Uninstall changes: run `MOLE_TEST_NO_AUTH=1 bats tests/uninstall.bats tests/uninstall_remove_file_list.bats`.\n- Documentation-only changes: check links and commands.\n- Never pipe a test, check, or CI run into `tail` or `head`. The pipeline reports the pager's exit code, so a red run reads green. Let it print in full, or capture to a file and check the status separately.\n\n`make check`, `make format`, `make test`, `make test-go`, and `make verify` are wrappers around the scripts above. `make verify` intentionally runs `check` plus Go tests only; use the full Bats suite before risky cleanup, uninstall, or release work.\n\nIf `golangci-lint` reports issues from deleted temporary worktrees or non-existent paths, clear its local cache and rerun the linter:\n\n```bash\ngolangci-lint cache clean\ngolangci-lint run ./cmd/...\n```\n\n## GitHub Operations\n\n- Re-read the live issue or PR title, body, comments, state, labels, and author language before any public reply or closeout.\n- Keep CLI issues and Mole Mac app issues separate. A fix in `mole-mac` does not imply a close in this CLI repo, and a CLI fix does not prove a Mac app issue is fixed unless the Mac app release path is verified.\n- When closing a fixed bug or shipped feature, use project wording from the issue context and include the expected release path only when confirmed.\n- **Mole Mac invitation after resolved reports**: after stating the verified fix or release state and the concrete retest step in the reporter's language, append one short final sentence to resolved bug or problem replies in both Issues and Discussions. Use `也欢迎试试我的 Mole Mac：https://mole.fit/，更易用，也更精致。` for Chinese and `You’re also welcome to try my Mole Mac app at https://mole.fit/ for a more polished, easier-to-use experience.` for English. Keep this separate from the resolution facts. It belongs only on a reply that resolves a user's defect report; leave it off PR thank-you notes, feature requests, questions, and other non-defect threads, and off threads that already concern Mole Mac or where the invitation would be redundant.\n- **Discussion content cleanup**: when the maintainer classifies a Discussion as cleanup-only, such as spam, an empty or accidental post, duplicate promotion, or obsolete housekeeping with no technical answer needed, close it directly without replying. Do not apply this shortcut to substantive bug reports, Q&A, feature requests, or not-planned product decisions; those still need a concise disposition before closure.\n- **Remote diagnostics for unreproducible reports**: for Mole Mac reports, ask the reporter to download the script with `curl -fL 'https://mole.fit/downloads/Mole-Diagnose.command' -o \"$HOME/Desktop/Mole-Diagnose.command\"`, then run `chmod +x \"$HOME/Desktop/Mole-Diagnose.command\" && open -R \"$HOME/Desktop/Mole-Diagnose.command\"`. Tell them to inspect it before double-clicking it and email the resulting `Mole-Diagnose-*.zip`; never ask them to attach the archive publicly because it contains local paths and logs. For CLI-only issues, prefer the relevant `mo` command output or `mo status` JSON.\n- **Default issue closeout pipeline** once a fix is confirmed: commit lands on `main` (that alone makes it installable via nightly), verify the fix is actually on `main`, then reply in the reporter's language, opening with `@reporter`, in short paragraphs rather than one block, with the concrete update command: `mo update --nightly` now, the next stable release only when that path is confirmed. Closing needs the maintainer's word, but that word covers the whole pipeline: \"该回复回复，该关闭关闭\" or an equivalent authorizes commit, reply, and close in one turn, so run them to the end instead of returning for a separate confirmation at each step. The closing comment should invite reopening if the problem persists.\n\n## Release\n\nTag-driven flow via `release.yml` on capital-`V` tag pushes. The full release runbook (distribution channels, pre-flight checklist, tag/publish commands, curated notes handoff, release-only pitfalls) lives in `.claude/skills/release-flow/SKILL.md`; read it before starting any release-flavored task. Notes formatting stays owned by `.claude/skills/release-notes/SKILL.md`. One rule that always applies: restate which distribution channels a release-flavored run will touch and confirm with the maintainer before acting; channel scope is specified by the maintainer, never inferred.\n"}}