Skip to content

residue v0.0.12

Choose a tag to compare

@github-actions github-actions released this 09 Nov 18:57
· 39 commits to main since this release
v0.0.12
c901a0b

Install

curl -fsSL https://residue.whiteheap.com/install.sh | sh
brew install whiteheap/tap/residue

Either one; every other way is in docs/installation.md. The script also turns on automatic cleanup, which deletes worktrees without asking; residue restore brings each back for 30 days. To install without it, run curl -fsSL https://residue.whiteheap.com/install.sh | sh -s -- --no-service. Homebrew installs without it. Or download the file for your machine below. The packages and the FreeBSD archives arrive within half an hour of the release.

Machine File
macOS, Apple silicon residue-v0.0.12-aarch64-apple-darwin.tar.gz
macOS, Intel residue-v0.0.12-x86_64-apple-darwin.tar.gz
Linux, static (musl), x86_64 residue-v0.0.12-x86_64-unknown-linux-musl.tar.gz
Linux, static (musl), arm64 residue-v0.0.12-aarch64-unknown-linux-musl.tar.gz
Debian, Ubuntu residue_amd64.deb, residue_arm64.deb
Fedora, RHEL residue.x86_64.rpm, residue.aarch64.rpm
Arch Linux residue-x86_64.pkg.tar.zst, residue-aarch64.pkg.tar.zst
FreeBSD, x86_64 residue-v0.0.12-x86_64-unknown-freebsd.tar.gz
FreeBSD, aarch64 residue-v0.0.12-aarch64-unknown-freebsd.tar.gz

Check a download: shasum -a 256 -c SHA256SUMS --ignore-missing, then gh attestation verify FILE --repo whiteheap/residue-cli.

The Mac app: https://github.com/whiteheap/residue-ui/releases

Launch hardening: residue keeps more worktrees that are in use or hold what it cannot rebuild, stops on a mistyped command or an old Git before anything runs, replaces only the service you looked at, and comes with a skill for coding agents.

  • An .env inside a build folder is kept. An environment file inside an ignored directory a rule matches, such as build/.env or dist/.env.production, now keeps its worktree, as an ignored .env at the top always has: ignored: build/.env. Templates (.env.example) and files in node_modules and virtual environments do not count, and a rule naming the file itself (git config --add residue.regenerable build/.env) lets residue delete it (#158).
  • A bare repository's HEAD branch is kept. In the layout git clone --bare and then a git worktree add per branch makes, the worktree on the branch the bare repository's HEAD names is its main checkout in all but name. The head check now blocks it: on main, the branch the bare repository's HEAD names; kept like a main checkout (#156).
  • residue service uninstall and status take no arguments. residue service uninstall --dry-run uninstalled the service; now any argument but --help is a usage error and nothing runs (#156).
  • A mistyped command is a usage error. residue clean printed could not inspect: clean: No such file or directory and exited 0; now it says residue: clean is not a command, and there is no folder named clean here; commands: scan, explain, remove, restore, stats, watch, service (#156).
  • residue in the home folder scans the usual code folders, as restore does, and says which; it no longer walks the whole home folder. A PATH, ~ included, is scanned as given (#156).
  • An old Git stops service install and watch --once before they start, and the install script before it adds a package repository or asks for a password. A running watch says so once and waits for a newer Git, instead of failing every pass (#156).
  • service install and the install script say what automatic cleanup removes, that it deletes without the trash and for how long residue restore brings a worktree back, and how to preview it (#156).
  • A skill for coding agents. npx skills add whiteheap/residue-cli --skill residue -g gives Claude Code, Codex and other agents the steps to remove the worktree they created once a task is done, and what to do when residue keeps it (skills/residue/SKILL.md, #163).
  • service install --expect replaces only the service you looked at. residue service status prints a fingerprint of the service. Given it, install writes nothing and exits 4 when the service changed since, so the Mac app can no longer save over a change a terminal or brew upgrade made while its dialog was open. Installs and uninstalls now run one at a time (#162).
  • A process in a worktree keeps it in use whatever characters its path has, such as é, 漢字, an emoji or a backslash, in the worktree's name or a folder above it. On macOS, residue now matches the names lsof prints escaped. Where it cannot be sure how lsof prints a path, activity is unknown, which keeps the worktree. On FreeBSD, residue reads each process's working directory, executable and open files from the kernel (kern.proc.filedesc), byte for byte, instead of from procstat (#157).
  • On illumos, an open file whose path the kernel cannot give still keeps its worktree in use: residue finds the file by its inode. Where it cannot look, activity is unknown, and the detail names the process. A deleted file does not count (#160).
  • A worktree that holds a very long path is kept. Each probe names a path whole only below a length: 1,004 bytes on macOS, 4,096 on Linux and 1,024 elsewhere. A worktree with a path that long anywhere inside it is now unknown (long_path), so it is never a candidate, and says how long that path is. So is one whose paths would reach that length where residue deletes it, at its hidden name or in the trash, so the plan never lists a worktree that the check after the move then keeps (#165).
  • On FreeBSD, residue also reads the files each process has mapped into memory (kern.proc.vmmap). A file a process holds that the kernel cannot name is placed by its inode, as on illumos: in use when the worktree holds it, unknown (unnamed_file) when residue cannot tell. A process running as your user that the kernel will not show makes activity unknown (probe_failed), and the reason names the process, as every one does with security.bsd.unprivileged_proc_debug at 0. A process the kernel shows only to root whatever the settings, such as ssh-agent or the process sshd keeps for each login, is not inspected, as on Linux (#165).
  • A removal Git stopped partway says what is left. When git worktree remove fails after Git has dropped the worktree from its list, the reason says what is left at the hidden name and ends with the removal record's residue restore, instead of a line that moves it back. That restore recreates the worktree and notes the folder left behind, for you to look through and delete (#165).
  • For scripts and integrations (changed from 0.0.11):
    • With no command first, the first PATH, the first argument before -- that is neither an option nor an option's value, exits 2 when it has no / and names nothing that exists (residue clean, residue --json clean); a command there says it goes first (residue --json scan). residue -- clean still scans ./clean.
    • scan with no PATH, run from the home folder or above it, scans the usual code folders there and notes it on stderr (residue: note: scanned only the usual code folders in your home folder (...)); with none of them, or from / with HOME unset or /, it exits 2.
    • service status and service uninstall with any argument but -h or --help exit 2.
    • On a Git older than 2.36, service install exits 1 with residue: git X.Y is too old; residue needs git 2.36 or later, and watch --once exits 1 with <time> residue watch: git X.Y is too old; residue needs git 2.36 or later. A running watch logs <time> residue watch: git X.Y is too old; residue needs git 2.36 or later; checking again every 5 minutes once, keeps running, and once Git is new enough logs git X.Y is new enough now; starting and starts.
    • The head check blocks the worktree on the branch a bare repository's HEAD names (status block), and is unknown when that HEAD cannot be read.
    • service install prints removes:, deletes: and preview: lines before check:. watch --once logs one line when its pass removes, or would remove, nothing, saying why (nothing to remove now: ..., or no removal this pass: each worktree it tried was kept, as logged above).
    • An ignored entry a rule matches is "regenerable": false when it holds an environment file no rule names, and the ignored check blocks: ignored entries not known to be regenerable: build/ (2 files, 18 B; build/.env is an environment file no rule names).
    • The scan hint names the preview first: Automatic cleanup is off. Preview it: residue watch --once --dry-run · turn it on: residue service install.
    • service status prints fingerprint: HEX, 32 lowercase hex digits, after what the service runs and before freed:, also when no service is installed (exit 3); fingerprint: unknown (REASON) when it cannot read the service file. The first line and the log: line are unchanged.
    • service install --expect FINGERPRINT and --expect none are new. When the service is not the one named, install writes nothing, prints one line on stderr (residue service install: the service changed since that fingerprint was read, or another residue release printed it; nothing changed. Read it again: residue service status, or for none, residue service install: a service or its file exists, and --expect none installs only when neither does; nothing changed. See it: residue service status), and exits 4. A value that is neither none nor 32 lowercase hex digits, a missing value or a second --expect exits 2. A service it cannot read to compare, or a folder it cannot lock, exits 1. Without --expect, install is unchanged but for the lock below.
    • service install and service uninstall lock the folder of the service file (~/Library/LaunchAgents, ~/.config/systemd/user). One that finds it locked prints residue service install: waiting for another residue service install or uninstall to end (or uninstall), waits up to 30 seconds, then exits 1. Where the folder cannot be locked, a command without --expect warns on stderr and runs as before.
    • activity.reason gains the values unmatchable_path, unnamed_file and long_path.
    • On FreeBSD, activity.backend is sysctl, not procstat.
    • A remove result that is failed after Git dropped the worktree from its list has no Recover with: line. Its reason says what is left at the hidden name and ends with its removal record keeps its HEAD and reflog: residue restore RECORD, and that restore's notes add what its removal did not delete is still at PATH; residue leaves it there for you to look through and delete.

Details

  • The activity check now runs after the walk that sizes the worktree, at the plan and again after the move aside, with the probe's snapshot taken before the walk as before. The walk reports the longest path it listed, and the inode of each file another process holds unnamed that it finds, so on illumos and FreeBSD such a file is looked for without a second walk; the check that no process uses the worktree of an unfinished removal record (removed::unheld) runs that walk itself, so a long path there is unknown (long_path) too. A walk that could not read an entry leaves such a file unknown (unnamed_file). A path as long as the probe's limit makes activity unknown (long_path); a use the probe saw still comes first. Tests: activity::tests (the limit, the detail, and how an unnamed file is judged), fsinfo::tests::the_walk_reports_its_longest_path_and_each_wanted_file_it_lists, and scripts/contract-check.sh section 17, activity-long-path.
  • On FreeBSD, each process's kern.proc.vmmap is read after its kern.proc.filedesc, both into buffers kept for the whole snapshot. ps now also lists each process's real, effective and saved user ids, and its flags, so a process the kernel will not show is a failure only when all three ids are yours and it is not one the kernel shows to root alone (P_SUGID, P2_NOTRACE, or init). Tests: activity::tests (the kinfo and vmentry records, the ps line, which processes the kernel shows to root alone) and scripts/contract-check.sh section 18, activity-proc-debug-off, which runs as root in the FreeBSD job and also runs explain while the user has an ssh-agent and an SSH session to localhost.
  • The plan holds each worktree's longest path against the probe's limit at the longest path the worktree itself can have where residue deletes it (remove::deleted_at): .NAME.residue-removing-PID with a PID of up to ten digits, or the trash entry, whichever is longer, and the worktree's own path for one already in the trash. Tests: remove::tests, activity::tests, and scripts/contract-check.sh section 17b, activity-near-path-limit, where a worktree 10 bytes short of the limit is refused at the plan, with nothing moved.
  • scripts/recovery-check.sh gains opened-before-move (a file opened at the top of the worktree right before the move keeps it, put back) and remove-fails-unlisted (the reason, the folder left aside, and the restore that recreates the worktree and notes it).
  • The bare repository's HEAD branch is read once per repository, with git symbolic-ref -q HEAD in its Git directory, and only when the main entry is bare. A detached HEAD keeps nothing; a HEAD residue cannot read makes the check unknown, which keeps the worktree. scan, explain, remove's plan and its re-check just before each deletion all apply it. Tests: scripts/contract-check.sh section 15, bare-head-branch: scan, explain and remove keep the worktree on main with head blocking, and remove the one on feat; it fails on 0.0.11, where scan listed main as a candidate.
  • residue service decides what to run from its arguments in one function, before anything touches launchd or systemd: -h or --help anywhere prints the help, any other extra argument to status or uninstall prints residue service uninstall: unexpected argument X and Run \residue service --help` for usage.`, exit 2. In-crate tests cover every case without reaching either service manager.
  • service install --expect: the fingerprint is FNV-1a, 128 bits, over the residue release, launchd's loaded state on macOS, the service file's bytes or that there is none, and the path the binary the file starts resolves to or that it names none, each part with its length. It covers the resolved path, not the binary's version or inode: brew upgrade changes the path under the same link, and the Mac app copies its own residue over the binary its service starts right before each save, which would make every such save a mismatch. The release covers the binary that writes the new service, which is the one it then starts. install compares once before any work, to stop early, and again right before the write, under an exclusive flock(2) on the folder of the service file that it holds until it returns; uninstall takes the same lock. It locks the folder, not a file of its own, so every residue that writes that service file takes the same lock, whatever XDG_STATE_HOME says. Not covered: a writer that takes no lock, such as an older residue or an editor, between the last comparison and the write; a systemd drop-in, which residue never writes; and an upgrade in place (the same path, a new release) of a binary that the install replaces with another one, which leaves the fingerprint alone but does not change what the install writes. In-crate tests cover the fingerprint (each part changes it, a byte moved between parts too, and the same service gives the same one), --expect's parsing and decision, and the lock's wait and time limit. .github/service-smoke.sh step 3 runs a matching, a stale and a none install on macOS and Linux and checks that a refused one leaves the service file as it was.
  • A first word that is not a command was taken as a PATH to scan, and so was the first PATH after options (residue --json clean). Now, when no command comes first and that PATH has no / and names nothing that exists, it is a usage error listing the commands; a command named after an option (residue --json scan) is told to go first. An option's value (--regenerable clean) is never taken for that PATH. residue ., residue ~/src, an existing folder's name and residue -- clean scan as before. A first argument that is not UTF-8 was dropped and . scanned in its place; it is now scanned as given.
  • residue or residue scan with no PATH, from the home folder or above it, walked up to four levels of everything there. It now scans the folders watch defaults to that exist (~/src, ~/code, ~/Developer, ~/Projects and the rest residue watch --help lists), like restore. With HOME unset or /, from /, it scans nothing and says in one line to name the folder: residue: no PATH given, and HOME names no home folder; name the folder that holds your repositories: residue scan PATH. The Mac app always passes its folders, so it is unchanged.
  • residue watch --once logs one line when its pass removed nothing, or with --dry-run would remove nothing, saying why: no linked worktrees, no branch landed or a check keeps those that did, and the free space of each disk against --min-free, to one decimal (the disk is 10.3% free (removes below 10%)). Every free-space figure watch logs takes more decimals when one would put it on the other side of --min-free: 9.96% free, below 10%, never 10.0% free, below 10%. A running watch does not log it each pass. A pass that tried removals and removed none says no removal this pass: each worktree it tried was kept, as logged above.
  • explain outside a Git repository, or on a path that does not exist, says so in one line and what to pass in the next, instead of Git's error. no Git repositories found says how deep residue looked and suggests naming the folder.
  • The walk that sizes each ignored entry also notes the first environment file strictly inside it that is outside .git entries and package folders and that no rule names itself, at no extra file system cost. A directory a rule matches, or a collapsed directory whose every file a rule matches, is then judged kept, so scan, explain, remove's plan, its re-check before each deletion and watch all keep the worktree. An environment file that is itself the ignored entry is judged by the rules, as before. Tests: policy::tests (which names are environment files; a rule names a file only by its name or its whole path) and fsinfo::tests::an_environment_file_inside_an_ignored_folder_is_found.
  • residue --help shows residue service install|status|uninstall and Kept: branches, commits and the stash. residue service install --help starts by saying it takes residue watch's options.
  • watch checks the Git version before its first pass. watch --once on an old Git stops at once, exit 1, with the same message as scan. A running watch, the service, takes its lock, logs that message once and checks again every five minutes, then starts when Git is new enough; before, every pass's scan failed and was logged, and exiting instead would have had launchd and systemd start it again every minute. While it waits, an upgrade of residue still ends it, so the service starts the new release. When a scan or remove child fails before its JSON, the log shows the child's stderr alone.
  • A work-landed removal's log line adds how many commits on no remote-tracking ref stay on the branch, when there are any: ; 2 commits on no remote-tracking ref stay on branch fix.
  • Low disk: a removal's space can take a while to show as free, such as while an APFS local snapshot holds it. A later low-disk pass counted that space as still needed and removed more. Now each pass counts what low-disk removals on that filesystem freed in the last 24 hours, and free space does not show yet, toward its target, and logs that it waits for it: waiting for about 3.1 GB that removals freed in the last 24h to show up as free space (a filesystem snapshot can hold it). It never removes more than before. 24 hours is how long Time Machine keeps its local snapshots. Right after a pass's removals it reads free space again, so only what does not show by then waits, and each later reading takes off only what free space gained since the one before. It cannot tell space that showed up and was used again between two readings from space that never showed, and waits for that until the 24 hours pass. The count is cleared once the filesystem is above --min-free.
  • The install script stops before it changes anything on Windows (residue does not run on Windows; run this inside WSL), on a CPU with no build (it links to building from source) and without curl. On the apt path it stops before writing under /etc when apt offers a Git older than 2.36, and names sudo add-apt-repository ppa:git-core/ppa for Ubuntu 22.04. It skips service install, saying why, when git --version is older than 2.36. When service install fails, it says to fix what it named and run it again. .github/workflows/install.yml gains refusals (no curl, an armv7l CPU, Ubuntu 22.04's Git on the apt and the archive paths) and windows (Git Bash).
  • A build for a system other than Unix stops at compile time with residue runs on Unix-like systems only; on Windows, use WSL.
  • Release notes start with how to install: the script, Homebrew, and a table of the file for each machine, with the check commands and a link to the Mac app.