Skip to content

residue v0.0.4

Choose a tag to compare

@github-actions github-actions released this 06 Nov 22:43
· 122 commits to main since this release
v0.0.4
fda6a4f

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 DURATION on scan, explain and remove (30m, 12h, 2d, 1w; anything else, zero included, is a usage error). A worktree that changed within DURATION fails the new idle check, so it is keep, with a reason such as changed 3 hours ago (--idle 2d). A last change residue cannot read makes the check unknown, which keeps it too. The check is reported right after activity. Without --idle it 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) catches add, commit, checkout, reset, stash and fetch when no file changed. (3) catches commits and checkouts by the time Git recorded. Not the mtime of logs/HEAD: measured with Git 2.50.1 and 2.43.0, git gc in the main checkout rewrites every linked worktree's logs/HEAD even 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, tar and rsync -a set 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 diff and git log on 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 and git worktree remove without --force. Decision: remove --all --yes is a usage error without --idle, because an unattended sweep without it is exactly the case above. remove without --all still 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 under kept (N): and do not fail it. The remove JSON gains errors.
  • explain shows last change with the entry that set it (index in its Git directory, src/main.rs in the worktree, HEAD reflog: commit: fix login), replacing newest change on disk. The changed N ago fact in scan uses the same time. Every JSON document gains idle (threshold and seconds, or null); each worktree gains last_change (unix, at, source, what) and last_change_error. size.newest_mtime stays.
  • 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 --idle matters, and git worktree lock for sessions that may pause longer.
  • Fix: rules from Git config named the wrong source. A rule from the global config (~/.gitconfig or GIT_CONFIG_GLOBAL) was reported as This repository's git config residue.regenerable adds: ".env", so a reader looked in .git/config while the rule applied to every repository. residue now reads the key with git config --show-scope --show-origin and 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 gains rule_sources (rule, scope, origin) for its Git config rules, and each document gains a top-level rule_sources for the residue config file (residue_config) and --regenerable (command_line). regenerable_config is 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:02 in local time while explain said 2026-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 --regenerable does not change that.
  • A signed apt repository for Debian and Ubuntu at https://whiteheap.github.io/residue/apt, so sudo apt install residue works and apt upgrade keeps residue current. It holds the latest release for amd64 and arm64, and every release updates it.