Skip to content

feat(docs-tidy): quarterly plan-then-apply tidy of ~/Documents - #10

Merged
twistedmelonman merged 4 commits into
mainfrom
claude/feat-docs-tidy-3660166a
Oct 21, 2026
Merged

twistedmelonman merged 4 commits into
mainfrom
claude/feat-docs-tidy-3660166a

Conversation

@twistedmelonman

@twistedmelonman twistedmelonman commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

What

docs-tidy.py repeats the 2026-10-21 manual cleanup of ~/Documents every quarter, as plan, then approve:

  • com.asiago.docs-tidy (LaunchAgent) runs docs-tidy plan --notify at 10:00 on Jan 1, Apr 1, Jul 1 and Oct 1. It writes a plan to ~/.local/state/docs-tidy/ and posts a notification with the counts. If the plan fails, it posts a FAILED notification.
  • docs-tidy show prints the plan. Nothing moves until docs-tidy apply. Pass --skip SECTION to leave a section out; it can be repeated.

Plan sections:

  • archive: moves top-level items untouched since Jan 1 of (year - 4) to My Drive/Documents Archive. Each tax/<year> is its own unit. Items on a KEEP list never move.
  • snapshot-merge / snapshot: for Documents - <HOST> folders, merges unique files back into Documents and stashes duplicate copies.
  • credit: renames and tags credit reports, keeping any Finder tags the user added.
  • dupes (report only): lists identical files in different top-level folders. Apply never moves them. An earlier version kept the copy with the earliest mtime, and on real data that picked the wrong copy. Excluded from the list: git working trees, node_modules, bundles, and files under 1 KiB.

Apply copies each file, re-hashes the copy, then deletes the source. It refuses a plan older than 14 days, a missing Drive root, or too little free disk space. Before copying, it downloads cloud-only iCloud files with brctl and retries on ETIMEDOUT.

Design: docs/plans/2026-10-21-docs-tidy-design.md. The script is Python because it reads the DriveFS SQLite database, hashes files and writes binary plists.

Verification

  • bats tests/docs-tidy.bats: 41/41 pass, on Python 3.15 and 3.9.
  • Full bats tests/: 342/342 pass locally, measured at commit 15f0810.
  • shellcheck -S info: clean.
  • A test LaunchAgent read ~/Documents, the Drive mount and the DriveFS database with no TCC prompt.
  • A live dry run against the real ~/Documents took about 1 s and downloaded nothing. Result: archive 0, snapshot-merge 4, credit 0, and 18 dupe groups listed for review.

Known gaps (from review, not fixed)

  • If the credit rename log cannot be written, the pointer and cleanup steps do not run in that apply. The apply TSV is written before that point.
  • The re-apply test checks only that "unreadable" is absent. It does not check the per-section counts.
  • No test covers removing a partial destination after a copy fails partway. That needs fault injection.

Not in this PR

  • The README row for docs-tidy and the fix to its stale "LaunchAgents/ is a placeholder" line. The Pangram gate flagged that text, so Andrew will write it.
  • Install after merge: symlink docs-tidy.py to ~/.local/bin/docs-tidy and the plist into ~/Library/LaunchAgents, then bootstrap the agent. Rename the Drive folder Documents Archive (pre-2022) to Documents Archive.

Claude Code Bot added 4 commits October 21, 2026 14:42
Repeats the 2026-10-21 manual cleanup on a schedule. A LaunchAgent runs
`docs-tidy plan --notify` at 10:00 on Jan/Apr/Jul/Oct 1; nothing moves
until Andrew runs `docs-tidy apply`.

The plan covers four sections: archive (units untouched since Jan 1 of
year-4) to My Drive/Documents Archive, old machine snapshots merged or
stashed, cross-unit byte-identical dupes stashed to
My Drive/probably-dupes/<date>, and credit-report renames plus Finder
tags. Apply copies, re-hashes, then deletes; it refuses stale plans, a
missing Drive root, or too little free disk, and prefetches dataless
iCloud files with brctl (the manual run lost 9 files to ETIMEDOUT).

Python rather than Bash: the job reads DriveFS SQLite, hashes files and
writes binary plists.
- Credit retag keeps the user's own Finder tags (and colours); it only
  adds a missing canonical tag, and plans a retag only in that case.
- The rename log row is written at rename time; a tag-write failure is
  a note on an OK result, not an ERR.
- A snapshot file whose target unit is archived in the same plan is
  stashed, not merged back into the archived unit.
- Notifications pass title and message to osascript as argv, so no
  AppleScript escaping, and a nonzero exit is logged.
- apply writes its TSV before the pointer and cleanup steps.
- Unreadable directories are reported, and their unit never archives.
- A failed or unverified copy removes its partial destination.
- The credit rename log is written with the apply TSV, before the
  pointer and cleanup steps, so a failure there cannot lose it.
- read_tags returns None (unknown) on a failed xattr read or a bad
  plist, so apply falls back to the planned tags instead of dropping
  the user's own tags. A missing xattr still means no tags.
- unit_newest treats an archive unit that is already gone as moved,
  not unreadable; os.walk calls onerror for a missing top directory,
  which made a re-run skip finished units.
The keep-one rule (earliest mtime) picked the wrong copy on real data:
it would have kept a write-up in a scratch Codex output folder and
stashed the copy in Beacon Biosignals, and pulled 7 files out of a
deliberately assembled HELOC packet. No rule can tell which copy is the
one in use, so the plan now lists every copy in each cross-folder group
and apply never moves them. `dupes` is no longer an apply section, so
`--skip dupes` is rejected. Machine-snapshot de-duplication is
unchanged.
@twistedmelonman
twistedmelonman merged commit a0aa22f into main Oct 21, 2026
5 checks passed
@twistedmelonman
twistedmelonman deleted the claude/feat-docs-tidy-3660166a branch October 21, 2026 22:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant