worktree
Where every linked git worktree of this repository belongs and where each one is. The container is the primary checkout's sibling named with `-wt`, the path under it is the branch name with its hierarchy kept, and both are derived from git's own identity — the common directory, the registered worktrees, the branches — never from a registry, a configuration or the current directory. A worktree somewhere else is a typed diagnostic with a remedy; the migration that repairs it is a command-line operation of the same service.
The whole topology
The repository, the container, the trunk and how it was decided, every registered worktree with its standing (primary, canonical, misplaced, detached, missing), its uncommitted work, its upstream distance and the issue its branch provably names, every local branch with or without a worktree and whether it is eligible for cleanup, every diagnostic with its code and remedy, and the tallies. Read from git on every call: the topology changes outside this process.
- query
- behaviorally_verified
- benchmark required
- cache disabled
- MCP tool majordomus_worktrees
- MCP resource {"name": "worktrees", "uri": "majordomus://worktrees"}
- $ worktree topology
No parameters.
Try it
The port is whatever the server logged; 8741 is the one it asks for first. The values are the operation's first example.
curl -s "http://127.0.0.1:8741/api/v1/worktrees"
Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.
Responses
| status | body | meaning |
| 200 |
RepositoryTopology |
The whole topology: the result. |
| 400 |
ErrorBody |
invalid_input: The input does not fit the schema: a parameter of the wrong type, an unknown one, or a value the capability rejects. |
| 404 |
ErrorBody |
not_found: The input names something the repository does not hold. |
| 500 |
ErrorBody |
internal: The capability failed for a reason of its own; the message names it. |
| default |
ErrorBody |
Any other failure the transport reports: 405 method_not_allowed for another method on this path, 400 invalid_input for a body that is not a JSON object. |
One branch: where its worktree belongs and what is there
The canonical path of a branch, derived from its name alone, whether the branch exists, whether something occupies that path, the worktree holding the branch when one does, and what stands in the way of creating or migrating it. The answer for a branch that does not exist yet is the path `worktree create` would use.
- query
- behaviorally_verified
- benchmark required
- cache disabled
- MCP tool majordomus_worktree_inspect
- $ worktree inspect
Query parameters
| name | type | required | description | examples |
| branch |
string |
yes |
The branch, full name (`feature/improve-cli`). It need not exist: the canonical path
derives from the name alone. |
feature-branch: "feature/example" |
Try it
The port is whatever the server logged; 8741 is the one it asks for first. The values are the operation's first example.
curl -s "http://127.0.0.1:8741/api/v1/worktrees/inspect?branch=feature%2Fexample"
Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.
Responses
| status | body | meaning |
| 200 |
InspectReport |
One branch: where its worktree belongs and what is there: the result. |
| 400 |
ErrorBody |
invalid_input: The input does not fit the schema: a parameter of the wrong type, an unknown one, or a value the capability rejects. |
| 404 |
ErrorBody |
not_found: The input names something the repository does not hold. |
| 500 |
ErrorBody |
internal: The capability failed for a reason of its own; the message names it. |
| default |
ErrorBody |
Any other failure the transport reports: 405 method_not_allowed for another method on this path, 400 invalid_input for a body that is not a JSON object. |
What it would take to bring every worktree home
One step per misplaced worktree with a branch: where it is, where it belongs, how it would move, the uncommitted work that moves with it, and what blocks it; plus the exceptions the migration cannot address by design — detached worktrees, stale registrations, the primary checkout off the trunk — each with what a person does about it. Planning changes nothing; `majordomus worktree migrate` applies it with a fingerprint taken before and after every move.
- query
- behaviorally_verified
- benchmark required
- cache disabled
- MCP tool majordomus_worktree_migration_plan
- $ worktree migrate
No parameters.
Try it
The port is whatever the server logged; 8741 is the one it asks for first. The values are the operation's first example.
curl -s "http://127.0.0.1:8741/api/v1/worktrees/migration"
Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.
Responses
| status | body | meaning |
| 200 |
MigrationPlan |
What it would take to bring every worktree home: the result. |
| 400 |
ErrorBody |
invalid_input: The input does not fit the schema: a parameter of the wrong type, an unknown one, or a value the capability rejects. |
| 404 |
ErrorBody |
not_found: The input names something the repository does not hold. |
| 500 |
ErrorBody |
internal: The capability failed for a reason of its own; the message names it. |
| default |
ErrorBody |
Any other failure the transport reports: 405 method_not_allowed for another method on this path, 400 invalid_input for a body that is not a JSON object. |
Where this is, and whether that is where it belongs
One worktree — the repository's own, or the one holding the directory the caller names — with its standing, its branch, its canonical path, its uncommitted work counted, whether it is where it belongs, and how many errors the whole topology carries. What an agent reads before it starts, and what the guard decides on.
- query
- behaviorally_verified
- benchmark required
- cache disabled
- MCP tool majordomus_worktree_status
- $ worktree status
Query parameters
| name | type | required | description | examples |
| path |
string |
no |
A directory inside one of this repository's worktrees; the answer is about that
worktree. Relative to the primary checkout when relative (`.` is the primary
checkout itself). Default: the repository the server was started for. A path in
another repository is refused: the topology answers only about its own. |
primary-checkout: "." |
Try it
The port is whatever the server logged; 8741 is the one it asks for first. The values are the operation's first example.
curl -s "http://127.0.0.1:8741/api/v1/worktrees/status?path=."
Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.
Responses
| status | body | meaning |
| 200 |
StatusReport |
Where this is, and whether that is where it belongs: the result. |
| 400 |
ErrorBody |
invalid_input: The input does not fit the schema: a parameter of the wrong type, an unknown one, or a value the capability rejects. |
| 404 |
ErrorBody |
not_found: The input names something the repository does not hold. |
| 500 |
ErrorBody |
internal: The capability failed for a reason of its own; the message names it. |
| default |
ErrorBody |
Any other failure the transport reports: 405 method_not_allowed for another method on this path, 400 invalid_input for a body that is not a JSON object. |
Uncommitted work in the linked worktrees, and what holds it
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
- benchmark required
- cache disabled
- MCP tool majordomus_worktree_uncommitted
- $ worktree uncommitted
Query parameters
| name | type | required | description | examples |
| older_than |
string |
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. |
one-hour: "1h" |
Try it
The port is whatever the server logged; 8741 is the one it asks for first. The values are the operation's first example.
curl -s "http://127.0.0.1:8741/api/v1/worktrees/uncommitted?older_than=1h"
Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.
Responses
| status | body | meaning |
| 200 |
UncommittedReport |
Uncommitted work in the linked worktrees, and what holds it: the result. |
| 400 |
ErrorBody |
invalid_input: The input does not fit the schema: a parameter of the wrong type, an unknown one, or a value the capability rejects. |
| 404 |
ErrorBody |
not_found: The input names something the repository does not hold. |
| 500 |
ErrorBody |
internal: The capability failed for a reason of its own; the message names it. |
| default |
ErrorBody |
Any other failure the transport reports: 405 method_not_allowed for another method on this path, 400 invalid_input for a body that is not a JSON object. |
Schemas
The components only this tag's operations use. Each row is the type's own field, its doc comment the description; a type another page owns links there.
BranchState
object
One local branch, with or without a work tree.
| property | type | required | description |
| name |
string |
yes |
The branch, short. |
| head |
string |
yes |
The commit it points at. |
| trunk |
boolean |
yes |
This branch is the trunk. |
| expected_path |
string | null |
no |
Where its work tree belongs. |
| worktree |
string | null |
no |
Where it is checked out, when it is. |
| upstream |
UpstreamState | null → UpstreamState |
no |
The upstream and its distance. |
| merged_into_trunk |
boolean | null |
no |
Reachable from the trunk. Absent when the trunk is unknown. |
| cleanup_eligible |
boolean |
yes |
Merged into the trunk, not the trunk, and either not checked out or checked out in a
work tree known to be clean. Derived state, never acted on automatically. |
| issue |
string | null |
no |
The issue this branch provably names. |
Used by RepositoryTopology
ContainerView
object
The container, as the topology reports it.
| property | type | required | description |
| path |
string |
yes |
The derived path. |
| suffix |
string |
yes |
The suffix it was derived with. |
| exists |
boolean |
yes |
It exists on disk. Created by the first worktree that needs it. |
Used by RepositoryTopology StatusReport
DiagnosticCode
"worktree.path_mismatch" | "worktree.container_occupied" | "worktree.nested" | "worktree.destination_conflict" | "worktree.missing" | "worktree.stale_registration" | "worktree.branch_already_checked_out" | "worktree.detached" | "worktree.ephemeral" | "worktree.primary_on_non_trunk" | "worktree.trunk_in_linked_worktree" | "worktree.path_escape" | "worktree.invalid_branch_name" | "worktree.locked" | "worktree.migration_verification_failed" | "worktree.trunk_unknown" | "worktree.case_collision" | "worktree.cross_device"
The stable machine name of everything that can be wrong with the topology. One code per
condition, reused by the command line, the API, MCP, the Cockpit, the tests and the
documentation; the prose beside a code is rendered from the typed state.
worktree.path_mismatchA linked work tree is not at its branch's canonical path.worktree.container_occupiedA linked work tree occupies the container path itself.worktree.nestedA linked work tree sits inside the primary checkout or inside another work tree.worktree.destination_conflictThe canonical path of a branch is occupied by something that is not its work tree.worktree.missingA registered work tree's directory does not exist.worktree.stale_registrationGit reports the registration as prunable.worktree.branch_already_checked_outA branch is checked out somewhere other than its canonical path.worktree.detachedA linked work tree has no branch.worktree.ephemeralA linked work tree is a session's scratch checkout: under the temporary directory or
under the primary checkout's .claude/worktrees/.worktree.primary_on_non_trunkThe primary checkout holds a branch that is not the trunk.worktree.trunk_in_linked_worktreeThe trunk is checked out in a linked work tree rather than the primary checkout.worktree.path_escapeA derived path would leave the container. Cannot happen for a valid branch name.worktree.invalid_branch_nameA branch name git accepted that this executable cannot derive a path for.worktree.lockedA work tree is locked, so it cannot be moved until it is unlocked.worktree.migration_verification_failedA move happened and the fingerprint after it differs from the one before.worktree.trunk_unknownThe trunk could not be determined.worktree.case_collisionTwo branch names map to one directory on a case-insensitive filesystem.worktree.cross_deviceA move crossed devices and was made by copy, repair and verification.
Used by TopologyDiagnostic
DirtyState
object
Uncommitted work in a work tree, counted from git status --porcelain. Untracked
content counts: it is exactly what a careless move loses.
| property | type | required | description |
| staged |
integer (uint) |
yes |
Entries with a change in the index. |
| unstaged |
integer (uint) |
yes |
Entries with a change in the work tree that is not in the index. |
| untracked |
integer (uint) |
yes |
Untracked, not ignored, files. |
| conflicted |
integer (uint) |
yes |
Entries with an unresolved merge conflict. |
| clean |
boolean |
yes |
Nothing above is non-zero. |
| in_progress |
string | null |
no |
A merge, rebase, cherry-pick, revert or bisect is in progress here. |
Used by MigrationStep UncommittedWorktree WorktreeState
EnvrcApproval
one of 6 variants
What became of the .envrc of a worktree that was just created, found, or moved.
variant direnv accepted it: the next cd loads the environment.
variant The worktree has no .envrc; there is nothing to approve and nothing is blocked.
variant direnv is not on the PATH; nothing was done, and nothing is blocked either, because
nothing would load the file.
| outcome | "direnv_absent" | yes | |
variant The worktree's .envrc is not the primary checkout's, so the approval given there
does not carry. direnv allow in the worktree, after reading it, is the person's.
variant The primary checkout's .envrc is not approved, so there is no approval to carry.
| outcome | "not_approved_in_primary" | yes | |
variant direnv was asked and refused, or could not be run; the message is its own.
| message | string | yes | direnv's standard error, or the error running it. |
| outcome | "failed" | yes | |
Used by MigrationStep
HooksPath
one of 6 variants
What became of the hooks directory of a worktree that was just created, found or moved.
It travels in the JSON of worktree create, ensure, migrate and repair, tagged by
outcome, so a reader branches on one word rather than parsing the line:
use majordomus_cli::worktree::HooksPath;
let set = HooksPath::Set {
path: "/r-wt/fix/x/.githooks".into(),
enabled: vec![],
};
let json = serde_json::to_value(&set).unwrap();
assert_eq!(json["outcome"], "set");
assert_eq!(json["path"], "/r-wt/fix/x/.githooks");
let back: HooksPath = serde_json::from_value(json).unwrap();
assert_eq!(back, set);
variant The repository's configuration sets no core.hooksPath: git's own hooks directory
in the common directory (or a value of the user's) serves every worktree, and there
is no tracked directory to follow.
| outcome | "not_configured" | yes | |
variant Git already resolves this worktree's hooks to its own directory; nothing was set.
| path | string | yes | The directory git runs the hooks from. |
| outcome | "own" | yes | |
variant This worktree's core.hooksPath was set, per worktree, to its own directory.
| path | string | yes | The directory it now runs the hooks from. |
| enabled | array of string | yes | Repository settings switched on so that a per-worktree value is honoured, as
key=value; empty when they were on already. |
| outcome | "set" | yes | |
variant The hooks directory is outside the primary checkout — one directory shared on
purpose — and every worktree runs it; nothing was set.
| path | string | yes | The directory every worktree runs the hooks from. |
| outcome | "shared" | yes | |
variant The primary checkout's hooks directory has no counterpart in this worktree's tree,
so the worktree keeps running the primary's: pointing it at a directory that does
not exist would switch its hooks off.
| missing | string | yes | The directory that would be the worktree's own, and is not there. |
| runs | string | yes | The directory the worktree runs the hooks from instead. |
| outcome | "untracked" | yes | |
variant Git was asked and refused, or could not be run; the message is its own.
| message | string | yes | What went wrong. |
| outcome | "failed" | yes | |
Used by MigrationStep
InspectReport
object
One branch, inspected: where its work tree belongs and what is there.
| property | type | required | description |
| branch |
string |
yes |
The branch asked about. |
| branch_exists |
boolean |
yes |
The branch exists locally. |
| expected_path |
string |
yes |
Where its work tree belongs. |
| destination_exists |
boolean |
yes |
Something exists at that path. |
| worktree |
WorktreeState | null → WorktreeState |
no |
The work tree that holds the branch, when one does. |
| canonical |
boolean |
yes |
The branch is checked out at its canonical path. |
| diagnostics |
array of TopologyDiagnostic → TopologyDiagnostic |
yes |
What stands in the way, if anything. |
Used by worktree.inspect
MigrationAction
"move" | "move_via_staging" | "copy_and_repair" | "none"
How a step moves its worktree.
moveOne git worktree move.move_via_stagingThe worktree is the container itself: out to a staging path, then into the container.copy_and_repairThe move crossed devices: copy, git worktree repair, verify, remove the original.noneNothing: the step is blocked.
Used by MigrationStep
MigrationPlan
object
A migration, planned or applied.
| property | type | required | description |
| schema |
string |
yes |
SCHEMA. |
| container |
string |
yes |
The container everything moves into. |
| steps |
array of MigrationStep → MigrationStep |
yes |
One step per misplaced worktree with a branch, container occupants first. |
| exceptions |
array of TopologyDiagnostic → TopologyDiagnostic |
yes |
Worktrees the migration cannot address by design: detached, missing, the primary
checkout off the trunk. Each says what a person does about it. |
| movable |
integer (uint) |
yes |
Steps that can be carried out as things stand. |
| blocked |
integer (uint) |
yes |
Steps that cannot. |
| moved |
integer (uint) |
yes |
Steps carried out and verified. |
| failed |
integer (uint) |
yes |
Steps attempted and not verified. |
| applied |
boolean |
yes |
Whether anything was changed. |
| moved_current |
string | null |
no |
The new path of the worktree the command was run from, when that one moved. |
Used by worktree.migration_plan
MigrationStep
object
One worktree to bring home.
| property | type | required | description |
| branch |
string |
yes |
The branch it holds. |
| from |
string |
yes |
Where it is. |
| to |
string |
yes |
Where it belongs. |
| head |
string | null |
no |
The commit checked out. |
| action |
MigrationAction → MigrationAction |
yes |
How it moves. |
| dirty |
DirtyState → DirtyState |
yes |
Its uncommitted work, which moves with it. |
| blockers |
array of TopologyDiagnostic → TopologyDiagnostic |
yes |
Why it cannot move, when it cannot. |
| outcome |
StepOutcome → StepOutcome |
yes |
What happened. |
| message |
string | null |
no |
What happened, in words, when it was not simply moved. |
| before |
WorktreeFingerprint | null → WorktreeFingerprint |
no |
The fingerprint before the move. |
| after |
WorktreeFingerprint | null → WorktreeFingerprint |
no |
The fingerprint after it. |
| differences |
array of string |
yes |
What differs between the two; empty when verified. |
| envrc |
EnvrcApproval | null → EnvrcApproval |
no |
What became of its .envrc under direnv once it was at its new path: the primary
checkout's approval carried there, or why it was not. Only on a moved step. |
| hooks |
HooksPath | null → HooksPath |
no |
Which hooks it runs at its new path: a per-worktree core.hooksPath names the old
one, so a moved worktree is bound again to its own tree's (I2718). Only on a moved
step. |
Used by MigrationPlan
OpenEpisode
object
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.
use majordomus_cli::worktree::OpenEpisode;
let unjudged = OpenEpisode {
id: "s-20260101000000-0a0a".into(),
last_sign_of_life: None,
stranded: None,
};
let wire = serde_json::to_value(&unjudged).unwrap();
assert!(wire.get("stranded").is_none(), "what could not be judged is absent, not false");
let back: OpenEpisode = serde_json::from_value(wire).unwrap();
assert_eq!(back, unjudged);
| property | type | required | description |
| id |
string |
yes |
The episode. |
| last_sign_of_life |
string | null |
no |
Its newest sign of life, RFC 3339 in UTC. |
| stranded |
boolean | null |
no |
Silent longer than the threshold. Absent when that cannot be judged — no threshold,
or no readable sign of life — and then it holds nothing. |
Used by UncommittedWorktree
RepositoryTopology
object
The whole topology: what worktree list, the MCP resource, the HTTP route and the
Cockpit all read.
| property | type | required | description |
| schema |
string |
yes |
SCHEMA. |
| repository |
RepositoryView → RepositoryView |
yes |
The repository. |
| container |
ContainerView → ContainerView |
yes |
The container every linked work tree belongs under. |
| trunk |
TrunkView → TrunkView |
yes |
The trunk. |
| observed_from |
string |
yes |
The work tree the call was answered from. |
| worktrees |
array of WorktreeState → WorktreeState |
yes |
Every registered work tree, primary first. |
| branches |
array of BranchState → BranchState |
yes |
Every local branch, by name. |
| diagnostics |
array of TopologyDiagnostic → TopologyDiagnostic |
yes |
Everything wrong with the topology, work trees first, then repository-wide. |
| tallies |
TopologyTallies → TopologyTallies |
yes |
The counts. |
| valid |
boolean |
yes |
No error-level diagnostic stands. |
Used by worktree.topology
RepositoryView
object
The repository's identity, as the topology reports it.
| property | type | required | description |
| primary_worktree |
string |
yes |
The primary checkout: what the container is named after. |
| git_common_dir |
string |
yes |
The common git directory: the repository's identity. |
| name |
string |
yes |
The primary checkout's directory name. |
Used by RepositoryTopology StatusReport
Standing
"primary" | "canonical" | "misplaced" | "detached" | "ephemeral" | "missing"
Where a work tree stands against the topology.
primaryThe primary checkout: exempt from the path rule, held to the trunk rule.canonicalA linked work tree at exactly its branch's canonical path.misplacedA linked work tree somewhere else. Migration brings it home.detachedA linked work tree with no branch. It has no canonical path and is never moved.ephemeralA linked work tree under the operating system's temporary directory or under the
primary checkout's .claude/worktrees/: a session's scratch checkout, owned and
removed by the harness that made it. Reported; migrated only on request.missingA registration whose directory is gone. Repair drops it.
Used by WorktreeState
StatusReport
object
The answer to "where am I, and is that where I belong".
| property | type | required | description |
| schema |
string |
yes |
SCHEMA. |
| repository |
RepositoryView → RepositoryView |
yes |
The repository. |
| container |
ContainerView → ContainerView |
yes |
The container. |
| trunk |
TrunkView → TrunkView |
yes |
The trunk. |
| worktree |
WorktreeState → WorktreeState |
yes |
The work tree the call came from, with its uncommitted work counted. |
| canonical |
boolean |
yes |
Whether this work tree is where it belongs: canonical, or the primary checkout on
the trunk, or detached. |
| repository_errors |
integer (uint) |
yes |
How many error-level diagnostics the whole topology carries. |
Used by worktree.status
StepOutcome
"planned" | "blocked" | "moved" | "failed"
What became of a step.
plannedPlanned and not applied.blockedNot carried out, with the blockers on the step.movedMoved, and the fingerprint after equals the one before.failedAttempted and not completed, or completed and not verified; the message says which.
Used by MigrationStep
Threshold
object
The age past which uncommitted work nothing holds is abandoned, and where it came from.
It is reported with every reading, so a reader can see what the verdicts were judged
against; a threshold nothing declares is carried as absent, not filled in.
use majordomus_cli::worktree::Threshold;
let declared = Threshold::resolve(Some("12h"), None).unwrap();
let wire = serde_json::to_value(&declared).unwrap();
assert_eq!(wire["declared"], "12h");
assert_eq!(wire["seconds"], 43_200);
let absent: Threshold =
serde_json::from_value(serde_json::json!({"source": "absent"})).unwrap();
assert_eq!((absent.declared, absent.seconds), (None, None), "absent is not a default");
| property | type | required | description |
| declared |
string | null |
no |
The value as written (12h). Absent when nothing declares one. |
| seconds |
integer | null |
no |
The same value, in seconds. |
| source |
string |
yes |
THRESHOLD_KEY, older_than when the caller named one, or absent. |
Used by UncommittedReport
TopologyDiagnostic
object
One thing wrong with, or worth knowing about, the topology.
| property | type | required | description |
| code |
DiagnosticCode → DiagnosticCode |
yes |
The stable code. |
| severity |
Severity → Severity |
yes |
How serious it is. |
| path |
string | null |
no |
The work tree involved. |
| branch |
string | null |
no |
The branch involved. |
| expected |
string | null |
no |
Where the work tree belongs. |
| message |
string |
yes |
What is wrong, in one line. |
| remedy |
string |
yes |
The command that addresses it. |
Used by InspectReport MigrationPlan MigrationStep RepositoryTopology WorktreeState
TopologyTallies
object
How many work trees and branches are in each state.
| property | type | required | description |
| worktrees |
integer (uint) |
yes |
Every registered work tree, primary included. |
| canonical |
integer (uint) |
yes |
Linked work trees at their canonical path. |
| misplaced |
integer (uint) |
yes |
Linked work trees somewhere else. |
| detached |
integer (uint) |
yes |
Linked work trees without a branch. |
| ephemeral |
integer (uint) |
yes |
Scratch checkouts of a session. |
| missing |
integer (uint) |
yes |
Registrations whose directory is gone. |
| locked |
integer (uint) |
yes |
Work trees git reports as locked. |
| dirty |
integer (uint) |
yes |
Work trees with uncommitted work, when it was asked for. |
| branches |
integer (uint) |
yes |
Local branches. |
| branches_without_worktree |
integer (uint) |
yes |
Local branches with no work tree. |
| cleanup_eligible |
integer (uint) |
yes |
Branches eligible for cleanup. |
| errors |
integer (uint) |
yes |
Error-level diagnostics. |
| warnings |
integer (uint) |
yes |
Warning-level diagnostics. |
Used by RepositoryTopology
TrunkSource
"remote_head" | "default_branch_config" | "conventional_name" | "primary_checkout" | "unknown"
Where the trunk was learned from, in the order it is looked for.
remote_headrefs/remotes/<remote>/HEAD: what the remote calls its default branch.default_branch_configinit.defaultBranch in the git configuration, and that branch exists locally.conventional_nameExactly one of main and master exists locally.primary_checkoutThe branch the primary checkout holds, because nothing else said.unknownNothing said; the trunk is unknown and every check that needs it says so.
Used by TrunkView
TrunkView
object
The trunk, as the topology reports it.
| property | type | required | description |
| branch |
string | null |
no |
The branch, when known. |
| source |
TrunkSource → TrunkSource |
yes |
How it was decided. |
| checked_out_at |
string | null |
no |
Where it is checked out. |
Used by RepositoryTopology StatusReport
UncommittedReport
object
Every linked work tree holding uncommitted work, with what holds it: the answer of
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 |
| schema |
string |
yes |
UNCOMMITTED_SCHEMA. |
| observed_at |
string |
yes |
When the reading was taken, RFC 3339 in UTC. |
| threshold |
Threshold → Threshold |
yes |
The threshold it was judged against. |
| worktrees |
array of UncommittedWorktree → UncommittedWorktree |
yes |
Every linked work tree with uncommitted work, abandoned first, then oldest first. |
| tallies |
UncommittedTallies → UncommittedTallies |
yes |
The counts. |
| findings |
array of string |
yes |
What could not be read or judged, in words. |
Used by worktree.uncommitted
UncommittedTallies
object
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.
use majordomus_cli::worktree::UncommittedTallies;
let wire = serde_json::json!(
{"measured": 4, "uncommitted": 3, "held": 1, "recent": 1, "abandoned": 1}
);
let t: UncommittedTallies = serde_json::from_value(wire.clone()).unwrap();
assert_eq!(t.held + t.recent + t.abandoned, t.uncommitted);
assert_eq!(serde_json::to_value(&t).unwrap(), wire, "the shape doctor's jq reads");
assert_eq!(UncommittedTallies::default().abandoned, 0, "nothing read, nothing abandoned");
| property | type | required | description |
| measured |
integer (uint) |
yes |
Linked work trees whose status was read. |
| uncommitted |
integer (uint) |
yes |
Of those, the ones holding uncommitted work: every entry of the listing. |
| held |
integer (uint) |
yes |
Held by a task, an episode or a process. |
| recent |
integer (uint) |
yes |
Held by nothing, younger than the threshold. |
| abandoned |
integer (uint) |
yes |
Held by nothing, older than the threshold. |
Used by UncommittedReport
UncommittedWorktree
object
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.
use majordomus_cli::worktree::{DirtyState, UncommittedWorktree, WorkStanding};
let entry = |path: &str, standing: WorkStanding, age: u64| UncommittedWorktree {
path: path.into(),
branch: None,
label: path.into(),
work: DirtyState { staged: 1, ..DirtyState::default() },
paths: 1,
oldest_change: None,
age_seconds: Some(age),
age: None,
task: None,
episodes: Vec::new(),
process_inside: None,
held_by: Vec::new(),
standing,
remedy: String::new(),
};
let mut listing = vec![
entry("/wt/held-old", WorkStanding::Held, 900_000),
entry("/wt/young", WorkStanding::Abandoned, 50_000),
entry("/wt/older", WorkStanding::Abandoned, 90_000),
];
majordomus_cli::order::canonical(&mut listing);
let paths: Vec<&str> = listing.iter().map(|w| w.path.as_str()).collect();
assert_eq!(paths, ["/wt/older", "/wt/young", "/wt/held-old"]);
| property | type | required | description |
| path |
string |
yes |
Absolute, as git holds it. |
| branch |
string | null |
no |
The branch checked out there. Absent when detached. |
| label |
string |
yes |
The branch, or detached/<short commit>. |
| work |
DirtyState → DirtyState |
yes |
The counts, as worktree status gives them. |
| paths |
integer (uint) |
yes |
Every changed path: staged, unstaged, untracked and conflicted together. |
| oldest_change |
string | null |
no |
When the oldest change was made: the earliest modification time among the changed
paths, the index file's standing in for a path that no longer exists. RFC 3339. |
| age_seconds |
integer | null |
no |
Seconds since then. |
| age |
string | null |
no |
The same, for a person (3d, 5h, 40m). |
| task |
string | null |
no |
The active task recorded there. |
| episodes |
array of OpenEpisode → OpenEpisode |
yes |
The episodes open there. |
| process_inside |
boolean | null |
no |
A process has its working directory inside it. Absent when that could not be read. |
| held_by |
array of string |
yes |
What holds it, in words; empty when nothing does. |
| standing |
WorkStanding → WorkStanding |
yes |
The verdict. |
| remedy |
string |
yes |
The command that would put this work on a remote ref. Suggested, never run. |
Used by UncommittedReport
UpstreamState
object
A branch's upstream and how far the two have moved apart, from for-each-ref in one
subprocess for every branch, never a fetch.
| property | type | required | description |
| name |
string |
yes |
The upstream ref, short (origin/master). |
| ahead |
integer | null |
no |
Commits here that the upstream lacks. |
| behind |
integer | null |
no |
Commits upstream that this branch lacks. |
| gone |
boolean |
yes |
The upstream ref no longer exists. |
Used by BranchState WorktreeState
WorkStanding
"held" | "recent" | "abandoned"
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.
use majordomus_cli::worktree::uncommitted::{judge, WorkStanding};
let standing = judge(false, Some(90_000), Some(43_200));
assert_eq!(standing, WorkStanding::Abandoned);
assert_eq!(serde_json::to_value(standing).unwrap(), "abandoned");
heldA task, an episode or a process holds it.recentNothing holds it, and it is younger than the threshold.abandonedNothing holds it, and it is older than the threshold — or its age cannot be read.
Used by UncommittedWorktree
WorktreeFingerprint
object
What a work tree held at one moment, reduced to digests.
| property | type | required | description |
| branch |
string | null |
no |
The branch, or none when detached. |
| head |
string | null |
no |
The commit. |
| index_digest |
string |
yes |
A digest of the index: every tracked path with its mode, blob and stage. |
| staged_diff_digest |
string |
yes |
A digest of the staged diff against HEAD. |
| unstaged_diff_digest |
string |
yes |
A digest of the unstaged diff against the index. |
| untracked_manifest_digest |
string |
yes |
A digest of every untracked file: path, kind, size and content. |
| untracked_files |
integer (uint) |
yes |
How many untracked files the manifest holds. |
| ignored_manifest_digest |
string |
yes |
A digest of every ignored entry: path, kind and size, never content. |
| in_progress |
string | null |
no |
The operation in progress, if any. |
| tree_manifest_digest |
string | null |
no |
A digest of every entry of the directory tree, ignored included: path, kind, size
and link target. Taken only for a move made by copying, where the rename guarantee
does not hold. |
Used by MigrationStep
WorktreeKind
"primary" | "linked"
Whether a work tree is the repository's own checkout or one linked to it.
primaryThe main work tree. It hosts the trunk, is never moved and is never removed.linkedA linked work tree. Belongs at its branch's canonical path.
Used by WorktreeState
WorktreeState
object
One work tree, as the topology sees it.
| property | type | required | description |
| path |
string |
yes |
Absolute, as git holds it. |
| kind |
WorktreeKind → WorktreeKind |
yes |
Primary or linked. |
| standing |
Standing → Standing |
yes |
Where it stands against the topology. |
| branch |
string | null |
no |
The branch checked out here, short. Absent when detached. |
| label |
string |
yes |
The name it is shown under: the branch, or detached/<short commit>. |
| head |
string | null |
no |
The commit checked out here. |
| detached |
boolean |
yes |
HEAD is detached here. |
| expected_path |
string | null |
no |
Where this branch's work tree belongs. Absent when detached, or for the primary
checkout on the trunk. |
| exists |
boolean |
yes |
The directory git registered still exists on disk. |
| current |
boolean |
yes |
The call came from inside this work tree. |
| locked |
string | null |
no |
Locked, with git's reason; an empty string when it recorded none. |
| prunable |
string | null |
no |
Prunable, with git's reason. |
| dirty |
DirtyState | null → DirtyState |
no |
Uncommitted work here. Absent when it was not asked for: it costs one subprocess per
work tree, and a topology check does not need it. |
| upstream |
UpstreamState | null → UpstreamState |
no |
The branch's upstream and its distance from it. |
| issue |
string | null |
no |
The issue this branch provably names: a path component of the branch equal to an
issue id of .ai/repo/project/issues/, or beginning with it and a hyphen. Nothing
is inferred from similarity. |
| diagnostics |
array of TopologyDiagnostic → TopologyDiagnostic |
yes |
What is wrong with this work tree, if anything. |
Used by InspectReport RepositoryTopology StatusReport