worktree.uncommitted
Every linked worktree whose index or working tree holds changes no commit carries: how many paths (staged, unstaged, untracked, conflicted), when the oldest change was made (the earliest modification time among the changed paths, the index file's standing in for a deleted one), and what holds the work — an active task record, an open episode that is not stranded, or a process whose working directory is inside. Work nothing holds and older than the threshold is `abandoned`, with the command that would put it on a remote `rescue/` ref; it is suggested, never run. The threshold is the policy's `session.stranded_after` unless `older_than` names another. Reads every worktree and writes none: every status runs without optional locks, so no index is refreshed.
Every linked worktree whose index or working tree holds changes no commit carries: how many paths (staged, unstaged, untracked, conflicted), when the oldest change was made (the earliest modification time among the changed paths, the index file's standing in for a deleted one), and what holds the work — an active task record, an open episode that is not stranded, or a process whose working directory is inside. Work nothing holds and older than the threshold is `abandoned`, with the command that would put it on a remote `rescue/` ref; it is suggested, never run. The threshold is the policy's `session.stranded_after` unless `older_than` names another. Reads every worktree and writes none: every status runs without optional locks, so no index is refreshed.
- query
- behaviorally_verified
- module worktree
- #worktree
- #git
- #continuity
- #introspection
Exposure
Where this one definition is reachable. Absence is explicit: a surface not listed does not carry it.
| surface | as |
|---|---|
| MCP | tool majordomus_worktree_uncommitted |
| HTTP | GET /api/v1/worktrees/uncommitted operationId worktree.uncommitted |
| command line | majordomus worktree uncommitted |
Schemas
The canonical input and output, from the Rust types; the MCP tool schema and the OpenAPI parameters and responses are derived from these.
input · UncommittedInput
| property | type | required | description |
|---|---|---|---|
| older_than | string | null | no | The age past which work nothing holds is abandoned, as a person writes it (90s,
30m, 12h, 1d). Default: the policy's session.stranded_after, which is also
how long an open episode may be silent and still hold the work. |
JSON Schema
{
"additionalProperties": false,
"description": "What to judge uncommitted work against.",
"properties": {
"older_than": {
"description": "The age past which work nothing holds is abandoned, as a person writes it (`90s`,\n`30m`, `12h`, `1d`). Default: the policy's `session.stranded_after`, which is also\nhow long an open episode may be silent and still hold the work.",
"type": [
"string",
"null"
]
}
},
"title": "UncommittedInput",
"type": "object"
}output · UncommittedReport
worktree.uncommitted on every surface, and what doctor renders.
use majordomus_cli::worktree::uncommitted::UNCOMMITTED_SCHEMA;
use majordomus_cli::worktree::{Threshold, UncommittedReport, WorktreeService};
let dir = tempfile::tempdir().unwrap();
let init = std::process::Command::new("git")
.args(["init", "-q"])
.current_dir(dir.path())
.status()
.unwrap();
assert!(init.success());
// a repository with no linked work tree, and a policy that declares no threshold
let threshold = Threshold::resolve(None, None).unwrap();
let report: UncommittedReport =
WorktreeService::open(dir.path()).unwrap().uncommitted(&threshold).unwrap();
assert_eq!(report.schema, UNCOMMITTED_SCHEMA);
assert!(report.worktrees.is_empty());
assert_eq!(report.tallies.measured, 0);
assert_eq!(report.findings.len(), 1, "a missing threshold is said, not assumed");| property | type | required | description |
|---|---|---|---|
| findings | array | yes | What could not be read or judged, in words. |
| observed_at | string | yes | When the reading was taken, RFC 3339 in UTC. |
| schema | string | yes | UNCOMMITTED_SCHEMA. |
| tallies | reference | yes | The counts. |
| threshold | reference | yes | The threshold it was judged against. |
| worktrees | array | yes | Every linked work tree with uncommitted work, abandoned first, then oldest first. |
JSON Schema
{
"$defs": {
"DirtyState": {
"description": "Uncommitted work in a work tree, counted from `git status --porcelain`. Untracked\ncontent counts: it is exactly what a careless move loses.",
"properties": {
"clean": {
"description": "Nothing above is non-zero.",
"type": "boolean"
},
"conflicted": {
"description": "Entries with an unresolved merge conflict.",
"format": "uint",
"minimum": 0,
"type": "integer"
},
"in_progress": {
"description": "A merge, rebase, cherry-pick, revert or bisect is in progress here.",
"type": [
"string",
"null"
]
},
"staged": {
"description": "Entries with a change in the index.",
"format": "uint",
"minimum": 0,
"type": "integer"
},
"unstaged": {
"description": "Entries with a change in the work tree that is not in the index.",
"format": "uint",
"minimum": 0,
"type": "integer"
},
"untracked": {
"description": "Untracked, not ignored, files.",
"format": "uint",
"minimum": 0,
"type": "integer"
}
},
"required": [
"staged",
"unstaged",
"untracked",
"conflicted",
"clean"
],
"type": "object"
},
"OpenEpisode": {
"description": "An open episode in a work tree, as far as holding its work goes: it holds the work only\nwhile `stranded` is `Some(false)`; a stranded or unjudgeable one holds nothing.\n\n```\nuse majordomus_cli::worktree::OpenEpisode;\nlet unjudged = OpenEpisode {\n id: \"s-20260101000000-0a0a\".into(),\n last_sign_of_life: None,\n stranded: None,\n};\nlet wire = serde_json::to_value(&unjudged).unwrap();\nassert!(wire.get(\"stranded\").is_none(), \"what could not be judged is absent, not false\");\nlet back: OpenEpisode = serde_json::from_value(wire).unwrap();\nassert_eq!(back, unjudged);\n```",
"properties": {
"id": {
"description": "The episode.",
"type": "string"
},
"last_sign_of_life": {
"description": "Its newest sign of life, RFC 3339 in UTC.",
"type": [
"string",
"null"
]
},
"stranded": {
"description": "Silent longer than the threshold. Absent when that cannot be judged — no threshold,\nor no readable sign of life — and then it holds nothing.",
"type": [
"boolean",
"null"
]
}
},
"required": [
"id"
],
"type": "object"
},
"Threshold": {
"description": "The age past which uncommitted work nothing holds is abandoned, and where it came from.\n\nIt is reported with every reading, so a reader can see what the verdicts were judged\nagainst; a threshold nothing declares is carried as absent, not filled in.\n\n```\nuse majordomus_cli::worktree::Threshold;\nlet declared = Threshold::resolve(Some(\"12h\"), None).unwrap();\nlet wire = serde_json::to_value(&declared).unwrap();\nassert_eq!(wire[\"declared\"], \"12h\");\nassert_eq!(wire[\"seconds\"], 43_200);\nlet absent: Threshold =\n serde_json::from_value(serde_json::json!({\"source\": \"absent\"})).unwrap();\nassert_eq!((absent.declared, absent.seconds), (None, None), \"absent is not a default\");\n```",
"properties": {
"declared": {
"description": "The value as written (`12h`). Absent when nothing declares one.",
"type": [
"string",
"null"
]
},
"seconds": {
"description": "The same value, in seconds.",
"format": "uint64",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"source": {
"description": "`THRESHOLD_KEY`, `older_than` when the caller named one, or `absent`.",
"type": "string"
}
},
"required": [
"source"
],
"type": "object"
},
"UncommittedTallies": {
"description": "The counts of an `UncommittedReport`: how many linked work trees were read, how many\nhold uncommitted work, and how those split into held, recent and abandoned. The three\nstandings always add up to `uncommitted`; `doctor` reads `abandoned` to decide whether it\nhas anything to warn about.\n\n```\nuse majordomus_cli::worktree::UncommittedTallies;\nlet wire = serde_json::json!(\n {\"measured\": 4, \"uncommitted\": 3, \"held\": 1, \"recent\": 1, \"abandoned\": 1}\n);\nlet t: UncommittedTallies = serde_json::from_value(wire.clone()).unwrap();\nassert_eq!(t.held + t.recent + t.abandoned, t.uncommitted);\nassert_eq!(serde_json::to_value(&t).unwrap(), wire, \"the shape doctor's jq reads\");\nassert_eq!(UncommittedTallies::default().abandoned, 0, \"nothing read, nothing abandoned\");\n```",
"properties": {
"abandoned": {
"description": "Held by nothing, older than the threshold.",
"format": "uint",
"minimum": 0,
"type": "integer"
},
"held": {
"description": "Held by a task, an episode or a process.",
"format": "uint",
"minimum": 0,
"type": "integer"
},
"measured": {
"description": "Linked work trees whose status was read.",
"format": "uint",
"minimum": 0,
"type": "integer"
},
"recent": {
"description": "Held by nothing, younger than the threshold.",
"format": "uint",
"minimum": 0,
"type": "integer"
},
"uncommitted": {
"description": "Of those, the ones holding uncommitted work: every entry of the listing.",
"format": "uint",
"minimum": 0,
"type": "integer"
}
},
"required": [
"measured",
"uncommitted",
"held",
"recent",
"abandoned"
],
"type": "object"
},
"UncommittedWorktree": {
"description": "One linked work tree holding uncommitted work: how much, how old, what holds it, the\nverdict, and the command that would preserve it. A listing is in canonical order —\nabandoned first, then recent, then held, the oldest first inside each.\n\n```\nuse majordomus_cli::worktree::{DirtyState, UncommittedWorktree, WorkStanding};\nlet entry = |path: &str, standing: WorkStanding, age: u64| UncommittedWorktree {\n path: path.into(),\n branch: None,\n label: path.into(),\n work: DirtyState { staged: 1, ..DirtyState::default() },\n paths: 1,\n oldest_change: None,\n age_seconds: Some(age),\n age: None,\n task: None,\n episodes: Vec::new(),\n process_inside: None,\n held_by: Vec::new(),\n standing,\n remedy: String::new(),\n};\nlet mut listing = vec![\n entry(\"/wt/held-old\", WorkStanding::Held, 900_000),\n entry(\"/wt/young\", WorkStanding::Abandoned, 50_000),\n entry(\"/wt/older\", WorkStanding::Abandoned, 90_000),\n];\nmajordomus_cli::order::canonical(&mut listing);\nlet paths: Vec<&str> = listing.iter().map(|w| w.path.as_str()).collect();\nassert_eq!(paths, [\"/wt/older\", \"/wt/young\", \"/wt/held-old\"]);\n```",
"properties": {
"age": {
"description": "The same, for a person (`3d`, `5h`, `40m`).",
"type": [
"string",
"null"
]
},
"age_seconds": {
"description": "Seconds since then.",
"format": "uint64",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"branch": {
"description": "The branch checked out there. Absent when detached.",
"type": [
"string",
"null"
]
},
"episodes": {
"description": "The episodes open there.",
"items": {
"$ref": "#/$defs/OpenEpisode"
},
"type": "array"
},
"held_by": {
"description": "What holds it, in words; empty when nothing does.",
"items": {
"type": "string"
},
"type": "array"
},
"label": {
"description": "The branch, or `detached/<short commit>`.",
"type": "string"
},
"oldest_change": {
"description": "When the oldest change was made: the earliest modification time among the changed\npaths, the index file's standing in for a path that no longer exists. RFC 3339.",
"type": [
"string",
"null"
]
},
"path": {
"description": "Absolute, as git holds it.",
"type": "string"
},
"paths": {
"description": "Every changed path: staged, unstaged, untracked and conflicted together.",
"format": "uint",
"minimum": 0,
"type": "integer"
},
"process_inside": {
"description": "A process has its working directory inside it. Absent when that could not be read.",
"type": [
"boolean",
"null"
]
},
"remedy": {
"description": "The command that would put this work on a remote ref. Suggested, never run.",
"type": "string"
},
"standing": {
"$ref": "#/$defs/WorkStanding",
"description": "The verdict."
},
"task": {
"description": "The active task recorded there.",
"type": [
"string",
"null"
]
},
"work": {
"$ref": "#/$defs/DirtyState",
"description": "The counts, as `worktree status` gives them."
}
},
"required": [
"path",
"label",
"work",
"paths",
"episodes",
"held_by",
"standing",
"remedy"
],
"type": "object"
},
"WorkStanding": {
"description": "What a work tree's uncommitted work is, judged: held by something, recent, or abandoned.\n`judge` is the one place it is decided, and `doctor` warns only on `abandoned`.\n\n```\nuse majordomus_cli::worktree::uncommitted::{judge, WorkStanding};\nlet standing = judge(false, Some(90_000), Some(43_200));\nassert_eq!(standing, WorkStanding::Abandoned);\nassert_eq!(serde_json::to_value(standing).unwrap(), \"abandoned\");\n```",
"oneOf": [
{
"const": "held",
"description": "A task, an episode or a process holds it.",
"type": "string"
},
{
"const": "recent",
"description": "Nothing holds it, and it is younger than the threshold.",
"type": "string"
},
{
"const": "abandoned",
"description": "Nothing holds it, and it is older than the threshold — or its age cannot be read.",
"type": "string"
}
]
}
},
"description": "Every linked work tree holding uncommitted work, with what holds it: the answer of\n`worktree.uncommitted` on every surface, and what `doctor` renders.\n\n```\nuse majordomus_cli::worktree::uncommitted::UNCOMMITTED_SCHEMA;\nuse majordomus_cli::worktree::{Threshold, UncommittedReport, WorktreeService};\nlet dir = tempfile::tempdir().unwrap();\nlet init = std::process::Command::new(\"git\")\n .args([\"init\", \"-q\"])\n .current_dir(dir.path())\n .status()\n .unwrap();\nassert!(init.success());\n// a repository with no linked work tree, and a policy that declares no threshold\nlet threshold = Threshold::resolve(None, None).unwrap();\nlet report: UncommittedReport =\n WorktreeService::open(dir.path()).unwrap().uncommitted(&threshold).unwrap();\nassert_eq!(report.schema, UNCOMMITTED_SCHEMA);\nassert!(report.worktrees.is_empty());\nassert_eq!(report.tallies.measured, 0);\nassert_eq!(report.findings.len(), 1, \"a missing threshold is said, not assumed\");\n```",
"properties": {
"findings": {
"description": "What could not be read or judged, in words.",
"items": {
"type": "string"
},
"type": "array"
},
"observed_at": {
"description": "When the reading was taken, RFC 3339 in UTC.",
"type": "string"
},
"schema": {
"description": "`UNCOMMITTED_SCHEMA`.",
"type": "string"
},
"tallies": {
"$ref": "#/$defs/UncommittedTallies",
"description": "The counts."
},
"threshold": {
"$ref": "#/$defs/Threshold",
"description": "The threshold it was judged against."
},
"worktrees": {
"description": "Every linked work tree with uncommitted work, abandoned first, then oldest first.",
"items": {
"$ref": "#/$defs/UncommittedWorktree"
},
"type": "array"
}
},
"required": [
"schema",
"observed_at",
"threshold",
"worktrees",
"tallies",
"findings"
],
"title": "UncommittedReport",
"type": "object"
}Policies
- benchmark
- required — a target on every transport the exposure declares; the cases are the input type's
- cache
- disabled — every call runs the handler
Benchmark targets
Derived from the registry for this repository: one requirement per transport, and the cases the input type provides. The whole matrix is on the benchmarks page.
| transport | state | cases | targets |
|---|---|---|---|
| direct | covered | 2 | policy-threshold one-hour |
| mcp | covered | 2 | policy-threshold one-hour |
| http | covered | 2 | policy-threshold one-hour |