Skip to main content

Module uncommitted

Module uncommitted 

Source
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.yaml with outcome active, the record start writes and finish closes), 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 the lsof reading worktree cleanup refuses 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§

OpenEpisode
An open episode in a work tree, as far as holding its work goes: it holds the work only while stranded is Some(false); a stranded or unjudgeable one holds nothing.
Threshold
The age past which uncommitted work nothing holds is abandoned, and where it came from.
UncommittedReport
Every linked work tree holding uncommitted work, with what holds it: the answer of worktree.uncommitted on every surface, and what doctor renders.
UncommittedTallies
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 to uncommitted; doctor reads abandoned to decide whether it has anything to warn about.
UncommittedWorktree
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§

WorkStanding
What a work tree’s uncommitted work is, judged: held by something, recent, or abandoned. judge is the one place it is decided, and doctor warns only on abandoned.

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.