diskr 0.1.53

Lightweight terminal file explorer and disk/storage manager for macOS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
# 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

| ID | Title | Type | Priority | Status |
| --- | --- | --- | --- | --- |
| [#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 | Open |
| [#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 | Open |
| [#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 | Open |
| [#50]#50-pageuppagedown-uses-the-file-pane-height-in-every-focus | PageUp/PageDown uses file-pane height everywhere | Bug, UX | Medium | Open |
| [#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 | Open |
| [#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 | Open |
| [#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 | In Progress (2026-06-12, Codex) |
| [#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 | Open |
| [#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

- **Type:** Bug, state integrity | **Priority:** High | **Effort:** ~half day
- **Source:** audit finding 48

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

- **Type:** Bug, state integrity | **Priority:** High | **Effort:** ~2-3 h
- **Source:** audit finding 63

`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

- **Type:** Bug, responsiveness | **Priority:** High | **Effort:** guard ~30 min; cancellation ~half day
- **Source:** audit finding 58

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

- **Type:** Bug | **Priority:** Medium | **Effort:** ~half day (free with finding 4C's queue)
- **Source:** audit finding 57

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.

### #52 Package filter cannot contain the letters `j` or `k`

- **Type:** Bug | **Priority:** Medium | **Effort:** ~10 min
- **Source:** audit finding 52

In `pkg_search_mode`, `j`/`k` match the navigation arms before the
character-push arm, so queries like `jq` or `kubectl` cannot be typed. File
search already gets this right — mirror it.

### #59 Batch-delete confirmation size ignores marked files

- **Type:** Bug | **Priority:** Medium | **Effort:** ~1 h
- **Source:** audit finding 59

`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

- **Type:** Bug | **Priority:** Medium | **Effort:** ~1 h
- **Source:** audit finding 61

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

- **Type:** Bug, UX | **Priority:** Medium | **Effort:** ~1-2 h
- **Source:** audit finding 62

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

- **Type:** Bug, accuracy | **Priority:** Medium | **Effort:** ~half day
- **Source:** audit finding 64

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

- **Type:** Bug, accuracy | **Priority:** Medium | **Effort:** ~2-3 h (or document the policy)
- **Source:** audit finding 65

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.

### #66 Cache invalidation never removes descendants

- **Type:** Bug, state integrity | **Priority:** Medium | **Effort:** ~1 h
- **Source:** audit finding 66

`invalidate_cache_for` walks ancestors but not descendants, so deleting a
directory leaves its children's sizes cached; recreating the same path later
serves the stale size without a stale marker. Retain-filter cached paths with
`starts_with(invalidated)`.

### #67 pip sizing mixes two Python environments

- **Type:** Bug, accuracy | **Priority:** Medium | **Effort:** ~half day
- **Source:** audit finding 67

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

- **Type:** Perf | **Priority:** Medium | **Effort:** ~1 h
- **Source:** audit finding 68

`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

- **Type:** Bug | **Priority:** Medium | **Effort:** ~1-2 h
- **Source:** audit finding 43

`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

- **Type:** Bug, CLI guardrail | **Priority:** Medium | **Effort:** ~1 h
- **Source:** audit finding 45

`--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

- **Type:** Bug | **Priority:** Medium | **Effort:** ~1 h
- **Source:** audit finding 25

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

- **Type:** Bug, accuracy | **Priority:** Medium | **Effort:** ~half day
- **Source:** audit finding 31

`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.

### #34 Project dependency rows double-count shared deps dirs

- **Type:** Bug, accuracy | **Priority:** Medium | **Effort:** ~1-2 h
- **Source:** audit finding 34

A project with both `pyproject.toml` and `requirements.txt` maps both
manifests to the same `.venv`, producing two rows and counting the bytes
twice. Group findings by `(project path, deps_dir)` and merge manifest labels.

### #50 PageUp/PageDown uses the file-pane height in every focus

- **Type:** Bug, UX | **Priority:** Medium | **Effort:** ~2-4 h
- **Source:** audit finding 50 (verified still present 2026-06-12: `page_move` at `src/app.rs:865` uses `files_area.height` for all panes)

Paging in Disks/Packages/Reclaim moves by the file pane's height and can wrap
via modulo arithmetic. Store per-pane visible row counts during render and
branch `page_move` by focus; prefer clamping over wrapping for page moves.

### #40 README and help text lag the actual TUI

- **Type:** Docs | **Priority:** Medium | **Effort:** ~1-2 h
- **Source:** audit finding 40; extended by finding 69

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

- **Type:** Docs | **Priority:** Medium | **Effort:** ~30 min on top of #40
- **Source:** audit finding 69

"~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.

### #49 Package-manager scans have no timeout or failure diagnostics

- **Type:** Robustness | **Priority:** Medium | **Effort:** ~1 day
- **Source:** audit finding 49

`run_command` has no timeout and discards stderr, so a hung `brew`/`npm`/`pip`
leaves the package pane loading forever and a failing manager renders as "0
packages". Return a structured result (stdout/stderr/status/timeout), add
per-command deadlines, and surface manager-level warnings in reports.

### #33 `diskr --packages` silently accepts nonexistent project paths

- **Type:** Bug, CLI | **Priority:** Low | **Effort:** ~15 min
- **Source:** audit finding 33

`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)

- **Type:** Chore | **Priority:** Low | **Effort:** small per item
- **Source:** audit finding 70

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

- **Type:** Debt | **Priority:** Low | **Effort:** ~half day
- **Source:** audit finding 23

`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.

### #71 Size-share bars and percentages use the scroll window as denominator

- **Type:** Bug, accuracy | **Priority:** Medium | **Effort:** ~1 h
- **Source:** 2026-06-12 UI/UX review

`draw_files` (`src/ui.rs`) computes `max_visible_size` and
`total_visible_size` over the windowed rows (`offset..end`), not the whole
directory: bars rescale as you scroll (the biggest row in view is always
full-width), and the `%` column is percent-of-window, so it neither sums to
100% for the directory nor stays stable while scrolling. Compute both
aggregates over all of `app.entries` (recompute when entries or sizes change,
not per camera move) and update the `file_size_bar`/column tests. Cheap fix —
do it first in the UX track (#71-#77); the bars are the most-watched
visualization in the product.

### #72 Help strip is focus-blind and overflows narrow terminals

- **Type:** UX | **Priority:** Medium | **Effort:** ~2-3 h on top of #56
- **Source:** 2026-06-12 UI/UX review; extends findings 12/56

`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

- **Type:** UX | **Priority:** Medium | **Effort:** ~1-2 days total; sub-items ship independently
- **Source:** 2026-06-12 UI/UX review

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

- **Type:** Feature, UX | **Priority:** Medium | **Effort:** ~1-2 days
- **Source:** 2026-06-12 UI/UX review

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

- **Type:** UX | **Priority:** Low | **Effort:** ~2-3 h
- **Source:** 2026-06-12 UI/UX review

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

- **Type:** UX | **Priority:** Low | **Effort:** ~half day
- **Source:** 2026-06-12 UI/UX review

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

- **Type:** UX, Refactor | **Priority:** Medium | **Effort:** ~2-3 days
- **Source:** 2026-06-12 UI/UX review

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

- **Type:** Chore, release integrity | **Priority:** Low | **Effort:** ~1-2 h
- **Source:** discovered 2026-06-12 while completing the orphaned v0.1.47 release

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.

### #82 Stale cached sizes are never revalidated; launches show a frozen snapshot

- **Type:** Bug, data freshness | **Priority:** High | **Effort:** ~half day
- **Source:** 2026-06-12 Milo report (same folders listed first every launch,
  unchanged across version upgrades); root-caused to audit finding 19's
  "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.

### #83 No way to refresh a single stale directory; `S` skips stale entries

- **Type:** Bug, UX | **Priority:** Medium | **Effort:** ~1-2 h
- **Status:** In Progress (2026-06-12, Codex)
- **Source:** discovered 2026-06-12 while root-causing #82; gap in finding
  19's "refresh on demand" contract, which only ever appeared in rationale
  prose and shipped solely as whole-view `r`

Both `scan_all_missing_visible` (`S`) and `scan_selected_missing_dir` filter
on `size.is_none()`, so stale-but-cached directories are excluded: the only
refresh is `r`, which invalidates and rescans every visible directory. Make
`S` include stale directories (matching its README description) and let the
selected-entry scan path rescan a stale selection, so one suspicious
directory can be verified without paying for the whole view.

### #84 Show growth since last scan (size cache as an automatic baseline)

- **Type:** Feature, UX | **Priority:** Medium | **Effort:** ~1 day
- **Source:** 2026-06-12 Milo request ("show more useful/relevant/recent
  info about the disk without dramatically slowing scanning")

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)

- **Type:** Feature, Perf | **Priority:** Medium | **Effort:** ~3+ days (audit estimate)
- **Source:** audit finding 19 Phase 2, deferred there as "a separate
  project" but never given a tracker entry; surfaced 2026-06-12 during
  #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).

### #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.