Repository navigation
residue v0.0.4
Scheduled sweeps. A machine running agents all day leaked 44 worktrees (4.2 GB) in two days, so the plan was a daily unattended residue remove. The activity check cannot see a paused agent session: harnesses such as Claude Code reset the shell to the main checkout after every command, so a worktree whose agent is waiting for approval has no process inside it, and if its work is pushed it is a candidate. A person sees the plan before deleting; a scheduled run does not.
--idle DURATIONonscan,explainandremove(30m,12h,2d,1w; anything else, zero included, is a usage error). A worktree that changed within DURATION fails the newidlecheck, so it iskeep, with a reason such aschanged 3 hours ago (--idle 2d). A last change residue cannot read makes the checkunknown, which keeps it too. The check is reported right afteractivity. Without--idleit does not run, and every 0.0.3 verdict and check is unchanged.- Decision, what counts as a change: the newest of (1) every entry the walk reaches in the checkout, ignored output included, (2) the files directly in the worktree's Git directory (
index,HEAD,ORIG_HEAD,FETCH_HEAD,COMMIT_EDITMSG), and (3) the timestamp inside the newest HEAD reflog entry. The walk already reads every entry, so (1) costs nothing; it now also records which entry is newest. (2) catchesadd,commit,checkout,reset,stashandfetchwhen no file changed. (3) catches commits and checkouts by the time Git recorded. Not the mtime oflogs/HEAD: measured with Git 2.50.1 and 2.43.0,git gcin the main checkout rewrites every linked worktree'slogs/HEADeven when nothing expires, so its mtime would keep leaked worktrees in an active repository forever. Not the branch tip's commit time: a branch checked out in a worktree moves only through that worktree, so the reflog and index already record it, and a commit made elsewhere can be older than the checkout that brought it in. Not ctime (touch,tarandrsync -aset mtime on purpose) and not atime (backups and code search read every file). Reads leave no trace: measured on both Git versions,git status,git diffandgit logon a settled worktree write nothing, and residue's own Git commands run with optional locks off, so a scan never resets the clock. The README tells people to lock a worktree that may sit without writes for longer than the idle time. Keeping a worktree that is still in use costs disk until the next run; removing one costs a paused agent its working directory, so the signal errs toward keep. residue remove --all PATH...treats each PATH as a scan root and removes every candidate under it, with the same plan, re-check andgit worktree removewithout--force. Decision:remove --all --yesis a usage error without--idle, because an unattended sweep without it is exactly the case above.removewithout--allstill takes only worktree paths, so a typo cannot turn into a sweep. A sweep exits 0 when every candidate was removed (or there was none) and 3 when a candidate was skipped or failed or part of a PATH could not be read; the worktrees it keeps are listed underkept (N):and do not fail it. The remove JSON gainserrors.explainshowslast changewith the entry that set it (index in its Git directory,src/main.rs in the worktree,HEAD reflog: commit: fix login), replacingnewest change on disk. Thechanged N agofact inscanuses the same time. Every JSON document gainsidle(threshold and seconds, or null); each worktree gainslast_change(unix,at,source,what) andlast_change_error.size.newest_mtimestays.- README: "Run it on a schedule", with a launchd LaunchAgent and a systemd user service and timer for a daily 04:00 sweep, how to preview the next run, why
--idlematters, andgit worktree lockfor sessions that may pause longer. - Fix: rules from Git config named the wrong source. A rule from the global config (
~/.gitconfigorGIT_CONFIG_GLOBAL) was reported asThis repository's git config residue.regenerable adds: ".env", so a reader looked in.git/configwhile the rule applied to every repository. residue now reads the key withgit config --show-scope --show-originand names each group of rules by scope and file:global config (~/.gitconfig) adds ".env"; this repository's config (~/src/app/.git/config) adds "web/.tscache". An invalid value names its file too. In JSON, each worktree gainsrule_sources(rule,scope,origin) for its Git config rules, and each document gains a top-levelrule_sourcesfor the residue config file (residue_config) and--regenerable(command_line).regenerable_configis unchanged: it is still the path of the residue config file, or null when there is none, and the schemas stay at v1. - Fix: times named no zone. The header said
as of 00:23:02in local time whileexplainsaid2026-11-06 22:23 UTC. Text output now uses local time with its UTC offset everywhere (as of 00:23:02 +02:00,2026-11-07 00:23 +02:00). JSON stays RFC 3339 UTC. - Fix: run from
/, as launchd and systemd start a job, residue printed every path relative to/, without its leading slash (private/tmp/...). Paths are now absolute there. - README: regenerable rules apply only to ignored entries. An untracked file that is not ignored always blocks, and
--regenerabledoes not change that. - A signed apt repository for Debian and Ubuntu at https://whiteheap.github.io/residue/apt, so
sudo apt install residueworks andapt upgradekeeps residue current. It holds the latest release for amd64 and arm64, and every release updates it.