Skip to content

devtrim 0.11.0

Choose a tag to compare

@github-actions github-actions released this 16 Oct 22:00
· 8 commits to main since this release
Immutable release. Only release title and notes can be modified.
d3e8691

A full review of the source against this Mac's real state. The read-only scan
offered 21.6 GiB while node-modules and artifacts failed outright on a
busy machine, and the toolchain the developer was using was offered for
removal. This release fixes what the review found, adds two read-only
reports, keeps the interface responsive, and cuts the test suite to the tests
that guard something (522 → under 300). It also ships the unpublished 0.10.10
fixes below.

Added

  • devtrim worktrees lists each repository's linked worktrees under the scan roots with their sizes: one git worktree list --porcelain -z per repository through the hardened Git invocation, the branch or detached HEAD, and the lock and prune reasons Git reports. A listed path is measured only when it is a real directory inside a scan root whose .git file leads back to that repository; anything else is listed unsized with the reason, and the main worktree is listed unsized. Sizes follow no link, stay on one device, never count a nested worktree twice and are lower bounds when entries cannot be read. The report never says safe, stale or removable, prints no total (cloned worktrees share blocks), adds no deletion authority, rejects every mutation flag and writes no journal
  • clean leftovers also lists the folders inside a repository's top-level .scratch when Git ignores that folder (git check-ignore) and tracks nothing in it, labelling a linked worktree there as one. Repositories are found anywhere under the roots, not only one level down
  • clean caches offers Gradle's caches and wrapper/dists under GRADLE_USER_HOME (default ~/.gradle; never the folder itself, which holds gradle.properties and init.d), offered only when each folder holds what Gradle writes there (caches/modules-2, a gradle-<version>-bin/-all distribution) and the Gradle home is not inside a Git repository, so GRADLE_USER_HOME alone never makes a folder a cache, and refused at scan and at apply while a Gradle daemon, build or Wrapper runs; the Go module cache at the path go env GOMODCACHE reports (queried with GOTOOLCHAIN=local, inside the home folder and holding Go's cache/download, asked again at apply; a permanent removal is refused while Go has left a folder read-only, and go clean -modcache is named); and the CocoaPods download cache ~/Library/Caches/CocoaPods, whose finding names pod install as what brings it back
  • clean xcode also offers watchOS DeviceSupport, tvOS DeviceSupport and visionOS DeviceSupport beside iOS DeviceSupport, with the same rules
  • codex_homes in ~/.config/devtrim.toml names more Codex homes (for example codex_homes = ["~/.codex-berlin"]) that agents judges exactly as ~/.codex, at scan and at apply. Nothing is inferred from folder names. An entry that is not an absolute folder inside the home folder, is reached through a link, overlaps ~/.codex or another entry, or does not exist refuses every command with an error naming the entry
  • The manual's "Keeping worktrees small" recipe: sparse checkouts, per-worktree installs from the package manager's store, APFS clones, and why cloned trees look larger than the space they hold

Fixed

  • toolchains offered the toolchain swiftly had in use: swiftly selects it through ~/.swiftly/config.json, not through a symlink, so the 4.6 GiB in-use swift-6.3.3-RELEASE.xctoolchain on the development machine looked unreferenced. Every toolchain swiftly lists as installed is now kept, at scan and at apply, and shown as report-only with swiftly uninstall <version> as its removal path; a swiftly configuration that cannot be read, parsed or mapped to a toolchain name withholds the whole category (swiftly 1.1.4 Config.swift, ToolchainVersion.swift; SWIFTLY_HOME_DIR honoured)
  • simulators made every scan exit nonzero on a Mac with the Command Line Tools but no Xcode. Exactly two xcrun answers now mean "nothing to manage", named in a diagnostic: xcrun simctl exiting 72 because there is no simctl, and xcrun --version exiting 1 with no developer directory. Any other failure is still an error
  • xcode failed the whole category when one DeviceSupport or idle-Xcode DerivedData folder could not be read or measured. That folder now offers nothing and is named in one error entry; the rest is offered
  • Every permanent removal (--shred, and trash-empty) of a tree holding a folder without write and search permission was refused part-way, after the files before that folder were gone, leaving the rest under a quarantine name; a Go module cache moved to the Trash and then purged is the ordinary case. The permanent preflight now checks every folder first and restores the target whole, naming the folder. In the Trash, where the operator confirmed the purge by its size, trash-empty instead gives a read-only folder the current user owns its owner's write permission just before emptying it, so the Go module cache clean caches moves there is purged; a folder owned by another user still refuses
  • caches: the .cache/node entry is narrowed to Corepack's own cache, .cache/node/corepack (COREPACK_HOME's default; corepack cache clean), and labelled as Corepack's. The npm finding says it is npm's whole cache folder (_cacache, _npx, _logs)
  • node-modules, artifacts and purge no longer fail on a busy Mac with "build processes changed again while their working directories were read". A build process that started during the liveness probe had to be read completely on one more lsof; a short-lived one that exited first refused the whole probe (9 of 30 read-only previews on the development machine, about 200 matching processes at load average 60–135). Each later lookup is now judged by the first round's rule — a process lsof did not report passes only when a fresh pgrep no longer lists it — up to five lsof runs in all; a list still changing after that refuses. The same loop then passed 30 of 30
  • The build-process probe trusted an lsof exit 0 even when it left a requested process out, so a build lsof did not report could lose its repository's dependencies; such a process is now judged like any unreported one (it passes only when a fresh pgrep no longer lists it). Pre-existing; found by the release's security review
  • trash-empty no longer fails, preview and apply, when one Trash item cannot be measured. That item offers nothing and is named in one error entry; the other items are listed and purged, and the run exits nonzero
  • A category whose scan failed no longer vanishes from the scan table, where it read as having nothing to offer. It keeps a row marked failed with the command that retries it
  • uninstall no longer drops its whole report when one folder inside a container is unreadable; that size is a disclosed lower bound. icloud names an unreadable folder as an error and still lists the files it could read
  • Names stored decomposed (NFD), such as café written as cafe plus a combining accent, print as written instead of cafe\u{301}. Leading marks, the invisible combining grapheme joiner U+034F, variation selectors, joiners, controls and bidirectional characters stay escaped
  • The TUI no longer freezes while a scan or apply runs. Both run on a worker thread; the working screen counts elapsed seconds. During a scan, q and Ctrl+C stop it: the terminal is restored, every command the scan started is killed, and devtrim exits 130. During an apply, q and Ctrl+C are refused until it returns, so no target is left half moved, and the screen says so. SIGINT and SIGTERM sent to the TUI process now restore the terminal, kill every started command and exit 130 or 143 instead of leaving the terminal in raw mode on the alternate screen; a second signal exits at once. Keys typed while work runs are still dropped, so type-ahead cannot approve a plan
  • In the TUI, a category whose scan failed is counted in the results title and named in the status with "r retries"; r also works on the Error screen. The title drops words before numbers so the mode, danger and failure count fit at 64 columns. s always turns permanent mode off (turning it on still needs a selected Trash item). A row without a path no longer reads "command action" unless it is a typed command. The Outcome and Error screens stop scrolling at their last line. The help overlay, README and manual say what Esc, q, r and signals do on each screen; Esc on the menu still quits
  • The non-interactive refusal of a danger-9 plan named -y, which cannot answer a typed size; it now names --yolo
  • Report-only commands (largest, icloud, uninstall) ended with "0 B actionable"; they now total what they report and say nothing is removed. Under scan --shred the table's hint names --shred. The CLI danger tag for 6–8 uses named ANSI red instead of a fixed RGB orange. The largest note claims a lower bound only when entries were skipped. status labels disk sizes as used and available instead of calling their sum the disk's size. A flag refusal's JSON envelope names the refusing operation (devtrim --json said unknown). --help says which commands honour the global mutation flags, and completions --help describes its argument
  • Documentation: the manual's quick-start and trash-empty samples now show the real output, the release binary is 2.6 MB (not 1.5 MB), the README says which Python cache names need no corroborating file, the site no longer claims "bundler outputs", and the docs say what --json does with a prompt on a terminal (stdout stays one document; an apply without -y or --yolo still asks on stderr)
  • Removed a stale doc comment that described a deleted count-objects helper

Changed

  • The test suite keeps only tests that fail when the code breaks in a way that matters to a user, to data or to security: 522 #[test] functions became 279 before this release's additions. Every test the mutation gate names was kept unchanged; 18 spot checks broke the production code a removed test covered and showed a kept test failing (one removal was overturned and kept)
  • New planted mutation cases: worktrees/common-dir, leftovers/scratch-ignored, trash/unmeasurable-item, tui/shred-off, toolchains/swiftly-{scan,apply,unparsable,unmapped}, simulators/no-simctl, xcode/unreadable-folder, caches/gradle-running-{scan,apply}, caches/gradle-repository, caches/gradle-layout, caches/go-namespace, sink/read-only-go-cache, sink/trash-permission-repair, liveness/lsof-silent-omission and agents/codex-homes-{scan,missing,identity}. The gate now refuses a marker that is a prefix of another before building anything (PV xcode/active-identity was one; it is now -spelling); the build-process successor case now removes the lookup bound

Fixed (unpublished 0.10.10)

  • Interrupting a CLI run with Ctrl-C (SIGINT) or SIGTERM now stops every command it started. Each command runs in a process group of its own, which the terminal's Ctrl-C never reaches, so an interrupted run used to leave a docker prune or a stuck query running. Every live group is now registered in src/process.rs; on either signal they are all killed before devtrim exits with 130 (SIGINT) or 143 (SIGTERM), and an apply attempt in flight stays in the journal without a result, which history reports as interrupted. A group is never signalled after its leader was reaped. The handler writes no message and a second signal exits at once, so a full or unread stderr can never stall the exit. A run started with SIGINT ignored (a background job of a non-interactive shell) now stops on SIGINT too, since there is no safe way to read the earlier disposition. The TUI handles the same signals itself (see Fixed above): it restores the terminal first, then kills every command and exits. The handler uses signal-hook, a crate already in the lockfile through crossterm
  • A running coding agent now protects the repository containing its working directory from node_modules and artifacts cleanup, as any build process does; a repository it edits by absolute path from elsewhere is not protected. The build-process names pgrep -x matches gain codex, claude, rust-analyzer and Claude Code's real process name, its version number (2.1.294 for ~/.local/share/claude/versions/2.1.294), matched only as a whole dotted triple of digits with an optional SemVer pre-release suffix such as 2.2.0-beta.1 (a process named like that qualifies, which can only protect or refuse more; 2.1, 1.2.3.4 and codex-code-mode do not match)
  • One cache that cannot be measured no longer fails the whole caches category. It is named in one error finding, as the project categories name folders their walk cannot read, so the run still exits nonzero and nothing in it is offered, while the other caches are listed as usual
  • Moves to the Trash use NSFileManager instead of Finder's AppleScript, which needed Automation permission for the calling process (it failed with that error on 2026-10-02) and was slow on a loaded machine. Every check around the move is unchanged. Finder's "Put Back" may not remember where an item came from; drag it out of the Trash to restore it. The deletion ast-grep rules now also reject a .delete(…) or .delete_all(…) Trash call outside the sink
  • External commands run under a wall-clock limit, so one stuck program no longer hangs a scan. Git, simctl, docker, npm and brew queries get two minutes, the pgrep and lsof liveness probes three (a system-wide lsof took 16 s at load average 300), and the typed Docker, simulator and maintenance commands an apply runs fifteen. Past its limit the command's whole process group is killed and the child reaped, so a helper such as docker's buildx plugin does not keep working after the failure is reported, and the repository, category or safety check that asked fails with an error naming the command and the limit; a timed-out liveness probe refuses like any failed one and never reads as "nothing running". Other categories still complete. Each command runs in a process group of its own. The rustix dependency gains its process feature for the group kill (no new crate). The no-unbounded-subprocess ast-grep rule rejects an unbounded output, status or spawn in src/