Expand description
Uncommitted work in the linked work trees, and whether anything still holds it (I1995).
Twice in two days finished work existed on one disk only: 45 uncommitted files in the
work tree of a session that had ended on 2026-10-07, 21 staged files in another on
2026-10-08. Neither had a commit or a remote ref, and both were found because somebody
happened to read that directory. doctor named branches whose commits reach no remote and
said nothing about an index that holds work no commit carries.
This is the reading that names them. For every linked work tree with staged, unstaged, untracked or conflicted paths it says how many, how old the oldest change is, and what holds the work — and from those, whether it is abandoned:
- held — an active task record (
.ai/local/state/current.yamlwith outcomeactive, the recordstartwrites andfinishcloses), an open episode (.ai/local/state/sessions-open/) that is not stranded, or a process whose working directory is inside it. The first two are the lifecycle’s own records, read with the readers that already exist for them (crate::session::open_episodes,Episode::stranded_after); the third is thelsofreadingworktree cleanuprefuses a removal on. Nothing new is stored. - abandoned — nothing holds it and its oldest change is older than the threshold.
- recent — nothing holds it and it is younger than that.
The threshold is session.stranded_after from the policy: the silence after which an
episode’s client is taken to be gone. One number answers both questions it is asked
here — whether an open episode still holds the work, and whether work nobody holds has
waited long enough to be called abandoned — so there is no second number to drift from
it. A caller with better evidence than a clock names its own (older_than).
The reading writes nothing. Every git status runs under GIT_OPTIONAL_LOCKS=0, so no
index refresh and no lock reaches any work tree, and another work tree’s .ai/local/ is
read, never written. What it suggests for an abandoned work tree — the command that puts
the work on a remote ref — is a suggestion; nothing here runs it.
§The lifecycle, end to end
A linked work tree gets a staged file; the same work is recent against a day and
abandoned against an hour once its file is years old; and an active task record then
holds it. That the readings leave every index byte-identical is the module’s own test.
use majordomus_cli::worktree::{Threshold, WorkStanding, WorktreeService};
use std::path::Path;
use std::process::Command;
let dir = tempfile::tempdir().unwrap();
let git = |cwd: &Path, args: &[&str]| {
let ok = Command::new("git")
.args(["-c", "user.name=t", "-c", "user.email=t@t", "-c", "commit.gpgsign=false"])
.args(args)
.current_dir(cwd)
.status()
.unwrap();
assert!(ok.success(), "git {args:?}");
};
let repo = dir.path().join("repo");
std::fs::create_dir_all(&repo).unwrap();
git(&repo, &["init", "-q"]);
git(&repo, &["commit", "-q", "--allow-empty", "-m", "init"]);
let left = dir.path().join("left");
git(&repo, &["worktree", "add", "-q", "-b", "work/left", left.to_str().unwrap()]);
std::fs::write(left.join("a.txt"), "a\n").unwrap();
git(&left, &["add", "a.txt"]);
let service = WorktreeService::open(&repo).unwrap();
let day = Threshold::resolve(None, Some("1d")).unwrap();
let fresh = service.uncommitted(&day).unwrap();
assert_eq!(fresh.tallies.uncommitted, 1, "the primary checkout is never listed");
assert_eq!(fresh.worktrees[0].work.staged, 1);
assert_eq!(fresh.worktrees[0].standing, WorkStanding::Recent);
// the file is made years old: against an hour, nothing holding it, it is abandoned
let touched = Command::new("touch")
.args(["-t", "202001010000"])
.arg(left.join("a.txt"))
.status()
.unwrap();
assert!(touched.success());
let hour = Threshold::resolve(None, Some("1h")).unwrap();
let old = service.uncommitted(&hour).unwrap();
assert_eq!(old.worktrees[0].standing, WorkStanding::Abandoned);
assert!(old.worktrees[0].remedy.contains("HEAD:refs/heads/rescue/work-left-"));
// an active task record holds it, however old it is
let state = left.join(".ai/local/state");
std::fs::create_dir_all(&state).unwrap();
std::fs::write(state.join("current.yaml"), "id: t-1\noutcome: active\n").unwrap();
let held = service.uncommitted(&hour).unwrap();
let entry = held.worktrees.iter().find(|w| w.label == "work/left").unwrap();
assert_eq!(entry.standing, WorkStanding::Held);
assert_eq!(entry.task.as_deref(), Some("t-1"));Structs§
- Open
Episode - An open episode in a work tree, as far as holding its work goes: it holds the work only
while
strandedisSome(false); a stranded or unjudgeable one holds nothing. - Threshold
- The age past which uncommitted work nothing holds is abandoned, and where it came from.
- Uncommitted
Report - Every linked work tree holding uncommitted work, with what holds it: the answer of
worktree.uncommittedon every surface, and whatdoctorrenders. - Uncommitted
Tallies - The counts of an
UncommittedReport: how many linked work trees were read, how many hold uncommitted work, and how those split into held, recent and abandoned. The three standings always add up touncommitted;doctorreadsabandonedto decide whether it has anything to warn about. - Uncommitted
Worktree - One linked work tree holding uncommitted work: how much, how old, what holds it, the verdict, and the command that would preserve it. A listing is in canonical order — abandoned first, then recent, then held, the oldest first inside each.
Enums§
- Work
Standing - What a work tree’s uncommitted work is, judged: held by something, recent, or abandoned.
judgeis the one place it is decided, anddoctorwarns only onabandoned.
Constants§
- THRESHOLD_
KEY - The policy key the threshold is read from.
- UNCOMMITTED_
SCHEMA - The schema of
UncommittedReport.
Functions§
- judge
- Judge one work tree’s work from what holds it, its age and the threshold. The one place the verdict is decided.
- rescue_
command - The command that puts a work tree’s uncommitted work on a remote ref named
rescue/<label>-<YYYYMMDD>: stage everything, commit it there, and push that commit to a branch of its own, so that the branch the work tree holds is not pushed over. The shape the two rescues of October 2026 took (origin/rescue/completion-doctrine-20261008). - shell_
quote - A path quoted for a POSIX shell.