# diskr Issue Tracker
Living tracker for bugs, debt, and small improvements. Product-level direction
lives in [ROADMAP.md](../ROADMAP.md); shipped changes are recorded in
[CHANGELOG.md](../CHANGELOG.md). The full workflow protocol is in
[AGENTS.md](../AGENTS.md) — read it before editing this file.
Rules in brief:
- IDs are stable and never reused. #1-#70 came from the 2026-06 audit and its
rechecks ([docs/AUDIT.md](AUDIT.md)); new issues start at **#71**.
- Statuses: `Open` → `In Progress (date, who/session)` → `Resolved (version)`.
- When you resolve an issue: move its entry to the Resolved section, note the
version, and add a CHANGELOG entry under `[Unreleased]`.
- When you find a new bug: add it here with the next free ID, even if you fix
it immediately.
- When Milo requests a change that is not tracked here (a perf improvement, a
UI tweak, a new feature), add it as an issue first and work it through the
same lifecycle — the tracker records all work, not just audit findings.
## Index
| [#47](#47-empty-trash-is-global-even-when-the-reclaim-report-has-no-trash-finding) | Empty Trash is global even without a Trash finding | Bug, destructive-action UX | High | Resolved (0.1.52) |
| [#48](#48-stale-background-history-saves-can-overwrite-newer-baselines) | Stale history saves can overwrite newer baselines | Bug, state integrity | High | Open |
| [#63](#63-persistent-state-writes-are-non-atomic-history-load-failures-silent) | Non-atomic state writes; silent history load failures | Bug, state integrity | High | Open |
| [#58](#58-unguarded-uncancellable-background-walks-pile-up) | Unguarded, uncancellable background walks pile up | Bug, responsiveness | High | Open |
| [#82](#82-stale-cached-sizes-are-never-revalidated-launches-show-a-frozen-snapshot) | Stale cached sizes are never revalidated | Bug, data freshness | High | Open |
| [#57](#57-cursor-movement-cancels-in-flight-batch-scans) | Cursor movement cancels in-flight batch scans | Bug | Medium | Open |
| [#52](#52-package-filter-cannot-contain-the-letters-j-or-k) | Package filter cannot contain `j` or `k` | Bug | Medium | Resolved (0.1.54) |
| [#53](#53-enter-keep-in-searchfilter-does-not-keep-the-filter) | "Enter keep" does not keep the filter | Bug, UX | Medium | Resolved (0.1.53) |
| [#56](#56-help-overlay-finding-12-was-never-implemented) | `?` help overlay was never implemented | Bug, UX | Medium | Resolved (0.1.51) |
| [#59](#59-batch-delete-confirmation-size-ignores-marked-files) | Batch-delete confirmation size ignores marked files | Bug | Medium | Open |
| [#61](#61-reclaim-status-line-reflects-the-paths-modal-not-the-selected-finding) | Reclaim status reflects paths-modal, not selection | Bug | Medium | Open |
| [#62](#62-reclaim-detail-panel-overlaps-the-findings-list) | Reclaim detail panel overlaps the findings list | Bug, UX | Medium | Open |
| [#64](#64-mount-boundary-policy-misses-non-volumes-mounts-and-apfs-helper-volumes) | Mount-boundary policy gaps (helper volumes, custom mounts) | Bug, accuracy | Medium | Open |
| [#65](#65-scan-totals-exclude-symlinks-directory-blocks-and-stat-errors) | Scan totals exclude symlinks/dir blocks/stat errors | Bug, accuracy | Medium | Open |
| [#66](#66-cache-invalidation-never-removes-descendants) | Cache invalidation never removes descendants | Bug, state integrity | Medium | Resolved (0.1.57) |
| [#67](#67-pip-sizing-mixes-two-python-environments) | pip sizing mixes two Python environments | Bug, accuracy | Medium | Open |
| [#68](#68-history-diffing-is-on2-and-historyjson-re-parses-on-every-navigation) | History diff O(n²); history.json re-parsed per navigation | Perf | Medium | Open |
| [#43](#43-empty-package-filters-can-still-act-on-hidden-packages) | Empty package filters can act on hidden packages | Bug | Medium | Open |
| [#45](#45-snapshot-thinning-accepts-unvalidated-paths) | Snapshot thinning accepts unvalidated paths | Bug, CLI guardrail | Medium | Open |
| [#25](#25-relative-start-paths-break-parent-navigation-and-scoped-reports) | Relative start paths break navigation and reports | Bug | Medium | Open |
| [#31](#31-history-baselines-and-diffs-hide-unreadable-directories) | History baselines hide unreadable directories | Bug, accuracy | Medium | Open |
| [#34](#34-project-dependency-rows-double-count-shared-deps-dirs) | Project deps double-count shared deps dirs | Bug, accuracy | Medium | Resolved (0.1.56) |
| [#50](#50-pageuppagedown-uses-the-file-pane-height-in-every-focus) | PageUp/PageDown uses file-pane height everywhere | Bug, UX | Medium | Resolved (0.1.55) |
| [#40](#40-readme-and-help-text-lag-the-actual-tui) | README and help text lag the actual TUI (see also #69) | Docs | Medium | Open |
| [#69](#69-readme-factual-claims-have-drifted) | README factual claims have drifted | Docs | Medium | Open |
| [#49](#49-package-manager-scans-have-no-timeout-or-failure-diagnostics) | Package-manager scans have no timeout/diagnostics | Robustness | Medium | Resolved (0.1.58) |
| [#71](#71-size-share-bars-and-percentages-use-the-scroll-window-as-denominator) | Size bars/percentages use the scroll window as denominator | Bug, accuracy | Medium | Resolved (0.1.56) |
| [#72](#72-help-strip-is-focus-blind-and-overflows-narrow-terminals) | Help strip is focus-blind and overflows narrow terminals | UX | Medium | Open |
| [#73](#73-files-pane-density-pass) | Files pane density pass (header row, modified column, size colors, summary title, mark totals) | UX | Medium | Open |
| [#74](#74-inline-reclaim-classification-in-the-file-browser) | Inline reclaim classification in the file browser | Feature, UX | Medium | Open |
| [#77](#77-unify-the-overlay-architecture) | Unify the overlay architecture (views + detail drawer) | UX, Refactor | Medium | Open |
| [#81](#81-release-tags-v0133-v0146-are-missing-v0132-release-workflow-failed) | Release tags v0.1.33-v0.1.46 missing; v0.1.32 workflow failed | Chore, release integrity | Low | Open |
| [#83](#83-no-way-to-refresh-a-single-stale-directory-s-skips-stale-entries) | No per-directory refresh; `S` skips stale entries | Bug, UX | Medium | Resolved (0.1.53) |
| [#84](#84-show-growth-since-last-scan-size-cache-as-an-automatic-baseline) | Show growth since last scan (cache as baseline) | Feature, UX | Medium | Open |
| [#85](#85-fsevents-based-cache-invalidation-audit-finding-19-phase-2) | FSEvents-based cache invalidation (finding 19 Phase 2) | Feature, Perf | Medium | Open |
| [#33](#33-diskr---packages-silently-accepts-nonexistent-project-paths) | `--packages` accepts nonexistent paths | Bug, CLI | Low | Open |
| [#70](#70-minor-ux-and-code-hygiene-paper-cuts-grab-bag) | Minor UX and code-hygiene paper cuts (grab-bag) | Chore | Low | In Progress (2026-06-13, Codex) |
| [#75](#75-header-and-status-line-repeat-defaults-and-the-selected-row) | Header/status line repeat defaults and the selected row | UX | Low | Open |
| [#76](#76-side-column-is-always-visible-disks-pane-spends-four-rows-per-disk) | Side column always visible; disks spend four rows each | UX | Low | Open |
| [#23](#23-exact-pinned-ratatui-corewidgets-sub-crates) | Exact-pinned ratatui sub-crates | Debt | Low | Open |
## Open
Entries are short summaries; full root cause, repro, fix design, and test
plans live under the matching finding number in [docs/AUDIT.md](AUDIT.md).
### #48 Stale background history saves can overwrite newer baselines
History save workers write `history.json` before the UI validates the result,
so a slow stale worker can move the persisted baseline backwards even though
the UI discards its message. Split scan-record from store-record: workers
return a `ScanRecord`; the main thread persists only validated results. See
also #63 for atomic writes.
### #63 Persistent state writes are non-atomic; history load failures silent
`size-cache.json` and `history.json` are written with bare `fs::write` (torn
files on crash/full disk), and a corrupt history file silently presents as "no
baselines". Write via temp file + rename, and surface history load errors in
status the way the size cache already does.
### #58 Unguarded, uncancellable background walks pile up
History diff workers spawn on every navigation into a baselined cwd with no
in-flight guard, and orphaned top-files/reclaim/disk-info walks keep running
after Esc or supersede. Add the missing guard and thread the existing
`ScanCancellation` token through these workers.
### #57 Cursor movement cancels in-flight batch scans
Every `scan_all` bumps the shared cancellation generation, so landing the
cursor on an unsized directory outside the current batch cancels the batch
mid-walk and clears its spinners, then reports "scan complete". Make
cancellation explicit (per-scan tokens; only data-invalidating events cancel)
and merge `scanning` flags instead of overwriting.
### #59 Batch-delete confirmation size ignores marked files
`request_batch_delete` sums sizes from `size_cache`, which only holds
directory results, so marked files (the common case) contribute zero and the
confirm modal shows no size. Resolve marked paths through `entries` first and
render `≥` when any marked dir is unsized.
### #61 Reclaim status line reflects the paths-modal, not the selected finding
With the paths modal closed, the status line and Space/f/O action target still
read the stale paths-modal state, referencing a path from a previously opened
finding. Derive status and action target from the selected finding unless the
modal is open.
### #62 Reclaim detail panel overlaps the findings list
Two stacked centered rects put the always-on detail box exactly over the
middle of the findings list, hiding rows (and the selection highlight) behind
it. Replace with a vertical Layout split (list + detail) like the paths modal
footer.
### #64 Mount-boundary policy misses non-/Volumes mounts and APFS helper volumes
Device-boundary skipping only applies under `/Volumes`: scanning `/` counts
Preboot/VM/Update helper volumes into the system total, and custom-path
network/FUSE mounts are walked as local data. Allow exactly the root dev and
the data-volume dev, count skips in `skipped_mounts`.
### #65 Scan totals exclude symlinks, directory blocks, and stat errors
Only regular files are counted: symlink sizes and directories' own allocated
blocks contribute nothing, and per-entry attribute errors are skipped without
incrementing `inaccessible`, so diskr undercounts vs. `du`/Finder on
dir-heavy trees. Count them, or document "regular file content only" in the
README.
### #67 pip sizing mixes two Python environments
The package list comes from `pip3` while site-packages comes from plain
`python3` (mismatched on Homebrew/CLT/pyenv splits); `--user` installs always
size as `?`; and dist-info prefix matching can pick `sentry-sdk` for `sentry`.
Derive list and paths from the same interpreter, include user site-packages,
and require a version digit after `name-`.
### #68 History diffing is O(n²) and history.json re-parses on every navigation
`diff_records` does linear scans per child (quadratic on 50k-child dirs), and
every navigation re-reads and re-parses the entire history file. Build a
HashMap over `before.children` and cache the parsed history in `App`.
### #43 Empty package filters can still act on hidden packages
`cached_pkg_visible_indices.is_empty()` means both "cache not built" and
"filter matches zero rows", so with an empty dependency-leaf filter the list
renders empty while Enter/`x`/`d` resolve against the full hidden package
list. Separate cache validity from contents so an empty cache after load means
zero visible rows everywhere.
### #45 Snapshot thinning accepts unvalidated paths
`--thin-snapshots` only checks `path.exists()` and passes the raw argument to
`tmutil`, accepting regular files, relative paths, and symlinks. Use the same
`canonical_dir` validation as `--space`, resolve the target volume via
`space::report_for_path`, and pass the resolved mount to the thin command.
### #25 Relative start paths break parent navigation and scoped reports
Start paths are validated but never canonicalized, so `diskr .` +
`Backspace` can navigate to an empty cwd, and `--reclaim .` from `$HOME`
misses HOME-relative fixed cache categories. Canonicalize start/report roots
immediately after validation.
### #31 History baselines and diffs hide unreadable directories
`history::scan_record` discards `DirScan::inaccessible`, so `--save`/`--diff`
report confident numbers when permissions blocked part of the tree (the TUI,
`--top`, and `--reclaim` already surface this). Add `inaccessible` to
`ChildSize`/`ScanRecord`/`DiffReport` with JSON/text warnings, minding schema
compatibility.
### #40 README and help text lag the actual TUI
README's key table and `print_help` are missing newer keys (`c`, `n`, `v`,
`a`, `R`, `t`, `B`, `E`, `u`, `x`, ...), have drifted in opposite directions,
and destructive actions are not distinguished from navigation. Drive README,
`print_help`, and the `?` overlay (#56) from one shared keymap table, with a
check that they stay in sync. Do #56, #40, and #69 together.
### #69 README factual claims have drifted
"~6,000 lines / no runtime beyond ratatui, crossterm, libc / no serde" is no
longer true (~14k lines; rayon, serde_json, anyhow are dependencies). Correct
the prose while doing #40's keymap work.
### #33 `diskr --packages` silently accepts nonexistent project paths
`print_packages` skips the `exists`/`is_dir` validation every other
path-scoped report does, so an invalid project root exits successfully with
only global package info. Add the same checks and include the canonical
project root in JSON output.
### #70 Minor UX and code-hygiene paper cuts (grab-bag)
Sixteen independent small items: `r` in Reclaim rescans the files pane, `p`
bypasses `set_focus`, Tab-transit triggers package scans, "grew" mislabeling,
bar-column alignment on scanning rows, char-count truncation breaking CJK
alignment, search match count not in the pane title, device-node disk labels,
stale FDA probe, silent empty-trash re-request, missing input cursor,
synchronous batch delete, stale `#[allow(dead_code)]`, `HOMEBREW_PREFIX`,
PEP 621 inline arrays. Fix opportunistically when touching the area, or split
into individual issues when picked up.
### #23 Exact-pinned ratatui-core/widgets sub-crates
`Cargo.toml` pins `ratatui-core =0.1.0` / `ratatui-widgets =0.3.0` with a
hand-rolled crossterm bridge in `src/terminal_backend.rs`, so no fixes ever
arrive. Migrate to mainline `ratatui` (deletes the bridge) or at least relax
to caret ranges; measure the binary-size cost before deciding.
### #72 Help strip is focus-blind and overflows narrow terminals
`draw_help` renders one static line of ~22 bindings regardless of focus, so
it truncates on narrow terminals and buries the relevant keys (package keys
show while browsing files; modal keys never show). Make the strip
context-sensitive: ~7 bindings for the focused pane or open modal, plus `?`
pointing at the full overlay (#56). Drive the strip, the `?` overlay, README
key table, and `print_help` from the single shared keymap table planned in
#40/#56/#69 so they cannot drift again.
### #73 Files pane density pass
Five independent upgrades to the main surface:
- (a) one-line column header (`NAME · SIZE ▾ · % · MODIFIED`) with a sort
indicator, replacing the `sort X` text in the app header and labeling the
currently unlabeled `%` column;
- (b) show the modified column whenever width allows, not only when
sort == Modified (`show_modified` in `draw_files`); optionally dim rows
with very old mtimes — old + big is the cleanup target;
- (c) color size text by magnitude (dim for KB, default for MB, bold/amber
for GB+) instead of uniform green, freeing bar color for #74's
classification;
- (d) pane title as a summary line: `files · 42 items · 13.2 GiB · 51/58
sized` — cwd total and scan coverage are visible nowhere today;
- (e) marks: render the type icon next to `✓` instead of replacing it, and
show `N marked · ≥X GiB` in the pane title or status line (running total;
coordinates with #59's size fix).
### #74 Inline reclaim classification in the file browser
The highest-value UX change identified by the review. The reclaim
classifier's knowledge only surfaces in the Reclaim overlay, but browsing the
file list is where the delete decision actually happens. Tag rows whose
name/path matches the existing fixed-location and artifact-name matchers in
`src/reclaim.rs` (`node_modules`, `target`, `.venv`, `__pycache__`,
`~/Library/Caches` children, ...) with a small class chip (`cache`, `build`)
colored safe-green / regenerable-yellow / risky-red. Name-based matching
only — no extra I/O on the render path. Same thesis as audit finding 15 ("the
engine knows more than the TUI shows"), applied to the main surface instead
of a modal. Column budget interacts with #73 (a); do them together or in
sequence.
### #75 Header and status line repeat defaults and the selected row
The header permanently shows `sort size · hidden off` — the defaults, noise
95% of the time — while `selection_status` reprints the selected row's
name/size/mtime, all already visible in the highlighted row. Show header
state only when non-default (`hidden` only when on; sort moves to #73's
column header); use the status line for what the row cannot show (full
resolved path, owner, symlink target, marked-items total) and give transient
status messages precedence with a timeout instead of appending them after
selection info. Add a legend for the `~`/`≥`/`*` size markers to the `?`
overlay (#56) and the file info popup — the markers are good density but
currently undiscoverable.
### #76 Side column is always visible; disks pane spends four rows per disk
The fixed 38% side column mostly idles ("press p to scan packages") while
file names truncate at 62% width, and each disk costs four rows of gauge.
Default to a collapsed side column — one row per disk
(`Macintosh HD ▓▓▓▓▓░░░ 72% 389G/1T · 112G free`) or fully hidden — expanding
only on Tab into Disks/Packages or `p`. The files pane gets the reclaimed
width for #73's columns. Considered alternative (rejected for now):
htop-style full-screen view switching for all panes; bigger rewrite, loses
ambient disk-fill context — revisit under #77 if the collapse proves
insufficient.
### #77 Unify the overlay architecture
Six modal types (reclaim, reclaim-paths, top-files, disk-info, pkg-detail,
file-info, plus confirms) have diverging footer/paging conventions;
`draw_top_files` and `draw_reclaim_paths` are ~100-line near-duplicates; and
Reclaim is a focus-cycle "pane" that actually renders as up to three stacked
overlays. Converge on two patterns: full-screen selectable list views for
list-like content (Reclaim findings; top files becomes a flat-view toggle of
the files pane sharing its keys and columns, like ncdu/dust), and a single
consistently-placed detail drawer for record-like content (file/pkg/disk
info, with low-value lines like hard links and xattr count collapsed behind
"more"). The shared selectable-list component is also what makes future views
(stale files, diff browser — ROADMAP) cheap. Do not block #62's quick layout
fix on this refactor; #50's per-pane paging work lands naturally here.
### #81 Release tags v0.1.33-v0.1.46 are missing; v0.1.32 Release workflow failed
The Release workflow run for v0.1.32 (2026-06-10) failed, and no tags were
pushed for v0.1.33 through v0.1.46 even though those versions are published
on crates.io — sessions evidently published manually during that window, so
fourteen versions have no git tag and no GitHub release. v0.1.47 went through
the workflow successfully on 2026-06-12, so the pipeline works again.
Remaining work: investigate why the v0.1.32 run failed (run 27312847934),
decide whether to backfill annotated tags for 0.1.33-0.1.46 by matching
crates.io versions to their bump commits (GitHub releases for them are
optional; crates.io is already authoritative), and note in AGENTS.md that
`gh run list --workflow Release` history before v0.1.47 is not a reliable
release record.
Update 2026-06-12: tags v0.1.54 and v0.1.55 exist, but both Release workflows
failed at the crates.io publish step with HTTP 429 "too many versions of this
crate in the last 24 hours"; crates.io still showed diskr 0.1.53 afterward.
Do not reuse those version numbers. When the rate limit clears, bump the
current Unreleased changes (including #34) to the next patch version and push
a fresh tag.
### #82 Stale cached sizes are never revalidated; launches show a frozen snapshot
"trust but mark stale" Phase 1 shipping with no revalidation path
Startup loads `size-cache.json` and marks every directory stale, but
`auto_scan` only scans entries with **no** size (`size.is_none()`,
`src/app.rs:1001`), so a fully cached view does zero I/O, reports
"cache hit · all sizes known", and sorts by values that may be weeks old.
The size-descending list is therefore a deterministic snapshot of past
scans — a directory that grew 40 GiB stays buried at its old rank until a
manual `r` — and the stale `~` marker carries the only hint. Fix
(stale-while-revalidate): when `auto_scan` finds nothing missing, feed
visible stale directories (oldest `scanned_at` first) through the existing
batch scan machinery; a shallow `stat` comparing dir mtime to `scanned_at`
may prioritize definitely-changed dirs but must not gate revalidation (deep
growth does not bubble mtime — finding 19's own rationale). Replace the
"cache hit" status with honest wording (e.g. "sizes from cache (oldest 9d)
· verifying…") and age-grade the stale styling. Coordinate with #57/#58's
cancellation work so background revalidation never preempts user-initiated
scans, and with #66 for invalidation correctness.
### #84 Show growth since last scan (size cache as an automatic baseline)
When a fresh `DirSize` lands in `drain_scan_results`, the previous cached
value is already in hand — a per-directory growth delta with zero extra
I/O. Render a delta badge (`+4.2G`) on rows that grew since their
`scanned_at` and add a growth sort mode alongside name/size/modified. This
attacks the anchoring problem directly: the eye goes to what changed, not
to the same familiar top-N (`~/Library` is always huge; "Downloads grew
18G since Tuesday" is the signal). Complements, not replaces, the manual
`--save`/`--diff` baselines in `history.rs`. Column budget interacts with
#73 (a)/(c) and #74; useful deltas depend on #82 producing fresh scans to
diff against.
### #85 FSEvents-based cache invalidation (audit finding 19, Phase 2)
#82's retrospective (see #86)
Persist the volume's last `FSEventStreamEventId` alongside the size cache;
on startup, replay events since that ID and mark only the touched subtrees
stale, so #82's revalidation rescans exactly what changed and unchanged
trees keep provably fresh sizes at near-zero steady-state cost. Must handle
`kFSEventStreamEventFlagMustScanSubDirs`, dropped events, and volume UUID
changes by falling back to #82's age-based revalidation. CoreServices C FFI
with a callback runloop thread — keep it isolated behind a small module.
## Resolved
Resolved issues are listed newest-first with the version that shipped the fix.
Entries may be pruned once they appear in a released CHANGELOG section;
findings #1-#70 keep their full write-ups in [docs/AUDIT.md](AUDIT.md).
### #49 Package-manager scans have no timeout or failure diagnostics — Resolved (0.1.58)
`run_command` now returns a structured `CommandResult` (stdout, stderr, exit
status, timed-out flag) instead of a bare `String`, with a poll-based timeout
(10s default, 30s for slow `brew cask` metadata). Timed-out or failed commands
surface a per-manager warning through `ManagerReport.warning`, displayed in the
TUI status line, CLI text output, and JSON reports. Stderr is no longer
discarded — the first line is included in diagnostics.
### #66 Cache invalidation never removes descendants — Resolved (0.1.57)
`invalidate_cache_for` now removes cached size metadata for the changed path
and every cached descendant before clearing ancestor aggregates, so deleting
and recreating a directory cannot revive stale child entries from the old
tree. Regression coverage locks both the ancestor-clearing and
descendant-clearing paths.
### #71 Size-share bars and percentages use the scroll window as denominator — Resolved (0.1.56)
`draw_files` now computes `max_visible_size` and `total_visible_size` over all
visible entries rather than only the on-screen window, so bar widths and `%`
values reflect each entry's share of the full directory and stay stable while
scrolling.
### #34 Project dependency rows double-count shared deps dirs — Resolved (0.1.56)
Project dependency scans now group manifests by project path and intended
dependency directory before sizing, merging manager and manifest labels for a
single row. Projects with both `requirements.txt` and `pyproject.toml` report
one `.venv` size once while still counting dependencies from both manifests;
regression coverage locks the shared-`.venv` case.
### #50 PageUp/PageDown uses the file-pane height in every focus — Resolved (0.1.55)
`page_move` now uses the active pane's visible rows instead of the Files pane
height everywhere: Files read their list viewport, Disks use the number of
rendered disk cards, Packages use the package-list height, and Reclaim uses
the findings-list height. Page jumps now clamp at the first/last visible row
instead of wrapping via modulo arithmetic, while single-row movement keeps its
existing cyclic behavior. Regression coverage exercises page moves in all four
focuses.
### #52 Package filter cannot contain the letters `j` or `k` — Resolved (0.1.54)
Package filter mode now handles plain character keys before navigation
shortcuts: `j` and `k` insert into the filter query just like file search,
while arrow keys still move the package cursor. A regression test covers
typing both `jq`-style and `kubectl`-style filters through the TUI key-routing
helper.
### #83 No way to refresh a single stale directory; `S` skips stale entries — Resolved (0.1.53)
`S` now scans visible directories whose cached size is stale as well as those
with no cached size, preserving known fresh sizes and cached stale values
while verification runs. Cursor-driven selected-directory scans use the same
predicate, so landing on a stale directory refreshes that one row without
requiring the whole-view invalidating `r` refresh. Regression coverage locks
both paths.
### #53 "Enter keep" in search/filter does not keep the filter — Resolved (0.1.53)
Enter in file search and package filter now keeps the narrowed list active
while leaving text input mode, so movement, marking, and package actions work
on the filtered subset. Esc and Ctrl+C clear the kept filter and restore the
full list; `/` reopens the current filter for editing.
### #47 Empty Trash is global even when the reclaim report has no Trash finding — Resolved (0.1.52)
`request_empty_trash` now refuses to arm unless the loaded reclaim report
contains a `Trash` finding, setting the status to "Trash is not in this
reclaim report" instead; the confirmation modal shows the finding's path and
size so what the user confirms matches what the report lists. Help-strip text
for `E` updated, README key table now documents `E`. Regression tests cover
both the refusing and arming paths.
### #56 Help overlay (finding 12) was never implemented — Resolved (0.1.51)
`?` opens a keyboard help overlay backed by `src/keymap.rs`, the same shared
keymap table used by the TUI footer and `--help` output. The footer now stays
short and points users to `? help` instead of trying to fit every shortcut on
one line. Esc, `?`, `q`, and Ctrl+C close the overlay.
### #30 History baseline refreshes run full scans on the UI thread — Resolved (0.1.45), verified 2026-06-12
Verified implemented despite the open tracker entry: the fix landed in commit
810eb1a (shipped in 0.1.45; the commit title references a different finding),
and only the AUDIT.md completion claim was reverted in 8083aad — the code
survived. Current state matches the audit's fix design in full:
`refresh_history_state` loads the saved baseline cheaply and schedules the
diff on a background `HistoryMsg` worker thread; `B` saves via the same
worker and reports "saving baseline..." immediately instead of blocking the
event loop; stale results are dropped by request id + cwd. Regression tests
`apply_history_baseline_schedules_background_diff` and
`stale_history_result_is_ignored_after_cwd_change` cover both behaviors and
pass. Residual gaps stay tracked where they were already filed: the diff
worker has no in-flight guard or cancellation (#58), history.json re-parses
on every navigation (#68), and stale background saves can overwrite newer
baselines (#48).
### #79 Replace rayon with a purpose-built scan pool — Resolved (0.1.50)
`src/pool.rs` replaces rayon with a std-only work-stealing pool: per-worker
deques (owners pop newest for subtree locality, thieves steal oldest),
a shared FIFO injector for external spawns, batched task pushes, idle-gated
wakeups, and sync waiters that help drain the queue like a blocked
`rayon::scope` caller. Locality proved load-bearing: a single shared queue
lost ~10% wall-clock on /Applications until the per-worker deques landed.
Verified at parity with the rayon build on wide (65k dirs/4.2M files), deep
(60-level chains), and /Applications corpora, with byte-identical `--top`
output. Drops rayon, rayon-core, crossbeam-deque, crossbeam-epoch, and
crossbeam-utils (81 -> 76 locked crates). Blocking scan_dir callers run on
scoped threads external to the pool, so the pool cannot deadlock.
### #78 Scan aggregation locks a global mutex per directory — Resolved (0.1.50)
Walk tasks now descend chains inline on one worker, accumulate the chain
locally, and merge into shared state once per chain instead of locking the
global aggregate per directory; sizes/counters are relaxed atomics and the
largest-files heap mutex is only touched when a top-files report is
requested. Chain descent also opens children via `openat(2)` on the held
parent fd (one component resolved instead of the full path). Wall-clock
gains were within noise on warm caches — the scanner is syscall-bound as
documented — but shared-state traffic and kernel path-resolution work are
strictly reduced.
### #80 Release binary uses thin LTO — Resolved (0.1.50)
`lto = "fat"`; with #79's dependency removal the release binary shrank
1,449,840 -> 1,333,392 bytes (-8.0%) on Apple Silicon.
### #55 Finding 28's rename re-selection fix was lost in a merge — Resolved (0.1.49)
Rename now reloads the file list while explicitly selecting the renamed path
instead of the stale pre-rename path, so name-sorted views keep the cursor on
the entry you just changed. Regression coverage restored via
`rename_reload_selects_new_path_after_sorting`.
### #86 Deferred audit phases went untracked once their parent finding was checked complete — Resolved (doc-only, 2026-06-12)
Process bug found while root-causing #82: audit finding 19 shipped Phase 1
("trust but mark stale"), was marked `[x] Completed`, and its explicitly
deferred Phase 2 (FSEvents) plus the "refresh on demand" requirement — which
existed only in the finding's rationale prose, never as a spec bullet — got
no tracker entry anywhere. The gap stayed invisible to every later audit
pass because closed items are not re-opened, and surfaced only when Milo
noticed launches showing identical stale listings. Fixed by adding workflow
rule 10 to AGENTS.md (deferred phases get their own issue before the parent
is marked complete; rationale-prose requirements must be promoted to spec
bullets or issues) and backfilling the lost work as #82-#85.
### #51 Key handlers ignore modifiers — Resolved (0.1.48)
A single early guard drops character keys carrying CONTROL/ALT/SUPER before
any dispatch arm (SHIFT still allowed), so Ctrl+C no longer triggers rename
and Ctrl+D no longer arms delete, and modified characters are not inserted
into text inputs. Ctrl+C now cancels the active input mode, confirmation
modal, or overlay exactly like Esc. README key table and `print_help`
updated in the same release.
### #54 A panic leaves the terminal in raw mode — Resolved (0.1.48)
`restore_terminal()` is shared by `TerminalGuard::drop` and a panic hook
installed before entering the TUI; the hook restores the terminal, then calls
the previous hook, so panic messages print to a sane shell even in release
builds where `panic = "abort"` skips destructors.
### #60 Package detail modal shows an unrelated system package — Resolved (0.1.48)
`PkgView::ProjectDeps` has its own detail rendering backed by
`selected_project_dep_detail()`; `selected_pkg_detail()` returns `None`
outside SystemManagers. The other ProjectDeps actions (`d`, `x`, reveal,
Quick Look) were audited and already resolved against the correct list.
### #46 Reclaim totals double-count nested fixed cache categories — Resolved (0.1.48)
Findings whose paths strictly contain another finding's paths are marked as
roll-ups, annotated `[subtotal]` in text/TUI output, and excluded from
`report.total`; JSON findings expose a new `rollup` field. Containment is
detected structurally by path, not by hardcoded category names.
### #44 `cargo test` can empty the real Trash — Resolved (0.1.48)
`empty_trash` delegates its `osascript` invocation to an injectable runner;
the destructive `empty_trash_runs` test is replaced by fake-runner tests
(argument capture and error propagation), so the full suite is safe to run
unskipped. The "reversible via Finder" doc comment is corrected.
### #42 Tree does not compile after overlapping partial merges — Resolved (pre-0.1.47)
Stale finding: `cargo check --locked` passes on a clean tree as of 2026-06-12;
the duplicate definitions described in the audit are gone.
### #37 Top-files/reclaim modals: broken paging and clipped footers — Resolved (0.1.44)
Verified implemented despite the audit's unchecked status: `modal_window_bounds`,
`page_top_files`, and `page_reclaim_paths` exist and are wired into rendering
(commit 8a3daba "Fix modal paging and footers").
### #16 File operations (rename, mkdir, multi-select, batch trash, Empty Trash) — Resolved (0.1.42), with caveats
The audit-listed gaps were tracked and fixed as #27 (mark visibility/staleness)
and #29 (Empty Trash confirmation). Caveats: the #28 rename-selection fix was
later lost in a merge (reopened as #55), and the batch-delete size summary
never landed for files (#59). Copy/move across directories was deliberately
left out of scope; see ROADMAP.md if that changes.