Skip to content
Cockpit0.29.0
A snapshot: read at commit 707a04c7ce8 on 2026-10-18T00:06:01Z by majordomus 0.29.0. Nothing on this page is read again. See it live

worktree.migration_plan

What it would take to bring every worktree home

What it is

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.

Descriptor

Canonical id
worktree.migration_plan
Module
worktree
Kind
query
Stability
behaviorallyverified
Provenance
declared
Source
apps/majordomus-cli/src/capability/builtin/worktree.rs
Cache
disabled
Benchmark
required
Tags
worktreegittopologymigration

Run

query
A query. It reads and changes nothing.

This capability takes no input.

GET /api/v1/worktrees/migration

Projections

InterfaceAsWhere
MCP toolmajordomus_worktree_migration_plan-
MCP resourcenot exposed-
HTTPGET /api/v1/worktrees/migrationSwagger
Command linemajordomus worktree migrate-

Schemas

Input · Empty
{
  "type": "object",
  "additionalProperties": false,
  "description": "No input.",
  "title": "Empty"
}
Output · MigrationPlan
{
  "type": "object",
  "properties": {
    "schema": {
      "type": "string",
      "description": "`SCHEMA`."
    },
    "container": {
      "type": "string",
      "description": "The container everything moves into."
    },
    "steps": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/MigrationStep"
      },
      "description": "One step per misplaced worktree with a branch, container occupants first."
    },
    "exceptions": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/TopologyDiagnostic"
      },
      "description": "Worktrees the migration cannot address by design: detached, missing, the primary\ncheckout off the trunk. Each says what a person does about it."
    },
    "movable": {
      "type": "integer",
      "format": "uint",
      "minimum": 0,
      "description": "Steps that can be carried out as things stand."
    },
    "blocked": {
      "type": "integer",
      "format": "uint",
      "minimum": 0,
      "description": "Steps that cannot."
    },
    "moved": {
      "type": "integer",
      "format": "uint",
      "minimum": 0,
      "description": "Steps carried out and verified."
    },
    "failed": {
      "type": "integer",
      "format": "uint",
      "minimum": 0,
      "description": "Steps attempted and not verified."
    },
    "applied": {
      "type": "boolean",
      "description": "Whether anything was changed."
    },
    "moved_current": {
      "type": [
        "string",
        "null"
      ],
      "description": "The new path of the worktree the command was run from, when that one moved."
    }
  },
  "required": [
    "schema",
    "container",
    "steps",
    "exceptions",
    "movable",
    "blocked",
    "moved",
    "failed",
    "applied"
  ],
  "description": "A migration, planned or applied.",
  "title": "MigrationPlan",
  "$defs": {
    "MigrationStep": {
      "type": "object",
      "properties": {
        "branch": {
          "type": "string",
          "description": "The branch it holds."
        },
        "from": {
          "type": "string",
          "description": "Where it is."
        },
        "to": {
          "type": "string",
          "description": "Where it belongs."
        },
        "head": {
          "type": [
            "string",
            "null"
          ],
          "description": "The commit checked out."
        },
        "action": {
          "$ref": "#/$defs/MigrationAction",
          "description": "How it moves."
        },
        "dirty": {
          "$ref": "#/$defs/DirtyState",
          "description": "Its uncommitted work, which moves with it."
        },
        "blockers": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/TopologyDiagnostic"
          },
          "description": "Why it cannot move, when it cannot."
        },
        "outcome": {
          "$ref": "#/$defs/StepOutcome",
          "description": "What happened."
        },
        "message": {
          "type": [
            "string",
            "null"
          ],
          "description": "What happened, in words, when it was not simply moved."
        },
        "before": {
          "anyOf": [
            {
              "$ref": "#/$defs/WorktreeFingerprint"
            },
            {
              "type": "null"
            }
          ],
          "description": "The fingerprint before the move."
        },
        "after": {
          "anyOf": [
            {
              "$ref": "#/$defs/WorktreeFingerprint"
            },
            {
              "type": "null"
            }
          ],
          "description": "The fingerprint after it."
        },
        "differences": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "What differs between the two; empty when verified."
        },
        "envrc": {
          "anyOf": [
            {
              "$ref": "#/$defs/EnvrcApproval"
            },
            {
              "type": "null"
            }
          ],
          "description": "What became of its `.envrc` under direnv once it was at its new path: the primary\ncheckout's approval carried there, or why it was not. Only on a moved step."
        },
        "hooks": {
          "anyOf": [
            {
              "$ref": "#/$defs/HooksPath"
            },
            {
              "type": "null"
            }
          ],
          "description": "Which hooks it runs at its new path: a per-worktree `core.hooksPath` names the old\none, so a moved worktree is bound again to its own tree's (I2718). Only on a moved\nstep."
        }
      },
      "required": [
        "branch",
        "from",
        "to",
        "action",
        "dirty",
        "blockers",
        "outcome",
        "differences"
      ],
      "description": "One worktree to bring home."
    },
    "MigrationAction": {
      "oneOf": [
        {
          "type": "string",
          "const": "move",
          "description": "One `git worktree move`."
        },
        {
          "type": "string",
          "const": "move_via_staging",
          "description": "The worktree is the container itself: out to a staging path, then into the container."
        },
        {
          "type": "string",
          "const": "copy_and_repair",
          "description": "The move crossed devices: copy, `git worktree repair`, verify, remove the original."
        },
        {
          "type": "string",
          "const": "none",
          "description": "Nothing: the step is blocked."
        }
      ],
      "description": "How a step moves its worktree."
    },
    "DirtyState": {
      "type": "object",
      "properties": {
        "staged": {
          "type": "integer",
          "format": "uint",
          "minimum": 0,
          "description": "Entries with a change in the index."
        },
        "unstaged": {
          "type": "integer",
          "format": "uint",
          "minimum": 0,
          "description": "Entries with a change in the work tree that is not in the index."
        },
        "untracked": {
          "type": "integer",
          "format": "uint",
          "minimum": 0,
          "description": "Untracked, not ignored, files."
        },
        "conflicted": {
          "type": "integer",
          "format": "uint",
          "minimum": 0,
          "description": "Entries with an unresolved merge conflict."
        },
        "clean": {
          "type": "boolean",
          "description": "Nothing above is non-zero."
        },
        "in_progress": {
          "type": [
            "string",
            "null"
          ],
          "description": "A merge, rebase, cherry-pick, revert or bisect is in progress here."
        }
      },
      "required": [
        "staged",
        "unstaged",
        "untracked",
        "conflicted",
        "clean"
      ],
      "description": "Uncommitted work in a work tree, counted from `git status --porcelain`. Untracked\ncontent counts: it is exactly what a careless move loses."
    },
    "TopologyDiagnostic": {
      "type": "object",
      "properties": {
        "code": {
          "$ref": "#/$defs/DiagnosticCode",
          "description": "The stable code."
        },
        "severity": {
          "$ref": "#/$defs/Severity",
          "description": "How serious it is."
        },
        "path": {
          "type": [
            "string",
            "null"
          ],
          "description": "The work tree involved."
        },
        "branch": {
          "type": [
            "string",
            "null"
          ],
          "description": "The branch involved."
        },
        "expected": {
          "type": [
            "string",
            "null"
          ],
          "description": "Where the work tree belongs."
        },
        "message": {
          "type": "string",
          "description": "What is wrong, in one line."
        },
        "remedy": {
          "type": "string",
          "description": "The command that addresses it."
        }
      },
      "required": [
        "code",
        "severity",
        "message",
        "remedy"
      ],
      "description": "One thing wrong with, or worth knowing about, the topology."
    },
    "DiagnosticCode": {
      "oneOf": [
        {
          "type": "string",
          "const": "worktree.path_mismatch",
          "description": "A linked work tree is not at its branch's canonical path."
        },
        {
          "type": "string",
          "const": "worktree.container_occupied",
          "description": "A linked work tree occupies the container path itself."
        },
        {
          "type": "string",
          "const": "worktree.nested",
          "description": "A linked work tree sits inside the primary checkout or inside another work tree."
        },
        {
          "type": "string",
          "const": "worktree.destination_conflict",
          "description": "The canonical path of a branch is occupied by something that is not its work tree."
        },
        {
          "type": "string",
          "const": "worktree.missing",
          "description": "A registered work tree's directory does not exist."
        },
        {
          "type": "string",
          "const": "worktree.stale_registration",
          "description": "Git reports the registration as prunable."
        },
        {
          "type": "string",
          "const": "worktree.branch_already_checked_out",
          "description": "A branch is checked out somewhere other than its canonical path."
        },
        {
          "type": "string",
          "const": "worktree.detached",
          "description": "A linked work tree has no branch."
        },
        {
          "type": "string",
          "const": "worktree.ephemeral",
          "description": "A linked work tree is a session's scratch checkout: under the temporary directory or\nunder the primary checkout's `.claude/worktrees/`."
        },
        {
          "type": "string",
          "const": "worktree.primary_on_non_trunk",
          "description": "The primary checkout holds a branch that is not the trunk."
        },
        {
          "type": "string",
          "const": "worktree.trunk_in_linked_worktree",
          "description": "The trunk is checked out in a linked work tree rather than the primary checkout."
        },
        {
          "type": "string",
          "const": "worktree.path_escape",
          "description": "A derived path would leave the container. Cannot happen for a valid branch name."
        },
        {
          "type": "string",
          "const": "worktree.invalid_branch_name",
          "description": "A branch name git accepted that this executable cannot derive a path for."
        },
        {
          "type": "string",
          "const": "worktree.locked",
          "description": "A work tree is locked, so it cannot be moved until it is unlocked."
        },
        {
          "type": "string",
          "const": "worktree.migration_verification_failed",
          "description": "A move happened and the fingerprint after it differs from the one before."
        },
        {
          "type": "string",
          "const": "worktree.trunk_unknown",
          "description": "The trunk could not be determined."
        },
        {
          "type": "string",
          "const": "worktree.case_collision",
          "description": "Two branch names map to one directory on a case-insensitive filesystem."
        },
        {
          "type": "string",
          "const": "worktree.cross_device",
          "description": "A move crossed devices and was made by copy, repair and verification."
        }
      ],
      "description": "The stable machine name of everything that can be wrong with the topology. One code per\ncondition, reused by the command line, the API, MCP, the Cockpit, the tests and the\ndocumentation; the prose beside a code is rendered from the typed state."
    },
    "Severity": {
      "oneOf": [
        {
          "type": "string",
          "const": "info",
          "description": "Worth knowing; changes nothing."
        },
        {
          "type": "string",
          "const": "warning",
          "description": "Something to look at; the object is still served."
        },
        {
          "type": "string",
          "const": "error",
          "description": "The file is excluded and the index is degraded."
        }
      ],
      "description": "How bad a diagnostic is. `Error` excludes the file it concerns from the index and puts\nthe index into the degraded state; `Warning` and `Info` do neither."
    },
    "StepOutcome": {
      "oneOf": [
        {
          "type": "string",
          "const": "planned",
          "description": "Planned and not applied."
        },
        {
          "type": "string",
          "const": "blocked",
          "description": "Not carried out, with the blockers on the step."
        },
        {
          "type": "string",
          "const": "moved",
          "description": "Moved, and the fingerprint after equals the one before."
        },
        {
          "type": "string",
          "const": "failed",
          "description": "Attempted and not completed, or completed and not verified; the message says which."
        }
      ],
      "description": "What became of a step."
    },
    "WorktreeFingerprint": {
      "type": "object",
      "properties": {
        "branch": {
          "type": [
            "string",
            "null"
          ],
          "description": "The branch, or none when detached."
        },
        "head": {
          "type": [
            "string",
            "null"
          ],
          "description": "The commit."
        },
        "index_digest": {
          "type": "string",
          "description": "A digest of the index: every tracked path with its mode, blob and stage."
        },
        "staged_diff_digest": {
          "type": "string",
          "description": "A digest of the staged diff against HEAD."
        },
        "unstaged_diff_digest": {
          "type": "string",
          "description": "A digest of the unstaged diff against the index."
        },
        "untracked_manifest_digest": {
          "type": "string",
          "description": "A digest of every untracked file: path, kind, size and content."
        },
        "untracked_files": {
          "type": "integer",
          "format": "uint",
          "minimum": 0,
          "description": "How many untracked files the manifest holds."
        },
        "ignored_manifest_digest": {
          "type": "string",
          "description": "A digest of every ignored entry: path, kind and size, never content."
        },
        "in_progress": {
          "type": [
            "string",
            "null"
          ],
          "description": "The operation in progress, if any."
        },
        "tree_manifest_digest": {
          "type": [
            "string",
            "null"
          ],
          "description": "A digest of every entry of the directory tree, ignored included: path, kind, size\nand link target. Taken only for a move made by copying, where the rename guarantee\ndoes not hold."
        }
      },
      "required": [
        "index_digest",
        "staged_diff_digest",
        "unstaged_diff_digest",
        "untracked_manifest_digest",
        "untracked_files",
        "ignored_manifest_digest"
      ],
      "description": "What a work tree held at one moment, reduced to digests."
    },
    "EnvrcApproval": {
      "oneOf": [
        {
          "type": "object",
          "properties": {
            "outcome": {
              "type": "string",
              "const": "approved"
            }
          },
          "required": [
            "outcome"
          ],
          "description": "direnv accepted it: the next `cd` loads the environment."
        },
        {
          "type": "object",
          "properties": {
            "outcome": {
              "type": "string",
              "const": "no_envrc"
            }
          },
          "required": [
            "outcome"
          ],
          "description": "The worktree has no `.envrc`; there is nothing to approve and nothing is blocked."
        },
        {
          "type": "object",
          "properties": {
            "outcome": {
              "type": "string",
              "const": "direnv_absent"
            }
          },
          "required": [
            "outcome"
          ],
          "description": "direnv is not on the PATH; nothing was done, and nothing is blocked either, because\nnothing would load the file."
        },
        {
          "type": "object",
          "properties": {
            "outcome": {
              "type": "string",
              "const": "differs"
            }
          },
          "required": [
            "outcome"
          ],
          "description": "The worktree's `.envrc` is not the primary checkout's, so the approval given there\ndoes not carry. `direnv allow` in the worktree, after reading it, is the person's."
        },
        {
          "type": "object",
          "properties": {
            "outcome": {
              "type": "string",
              "const": "not_approved_in_primary"
            }
          },
          "required": [
            "outcome"
          ],
          "description": "The primary checkout's `.envrc` is not approved, so there is no approval to carry."
        },
        {
          "type": "object",
          "properties": {
            "message": {
              "type": "string",
              "description": "direnv's standard error, or the error running it."
            },
            "outcome": {
              "type": "string",
              "const": "failed"
            }
          },
          "required": [
            "outcome",
            "message"
          ],
          "description": "direnv was asked and refused, or could not be run; the message is its own."
        }
      ],
      "description": "What became of the `.envrc` of a worktree that was just created, found, or moved."
    },
    "HooksPath": {
      "oneOf": [
        {
          "type": "object",
          "properties": {
            "outcome": {
              "type": "string",
              "const": "not_configured"
            }
          },
          "required": [
            "outcome"
          ],
          "description": "The repository's configuration sets no `core.hooksPath`: git's own hooks directory\nin the common directory (or a value of the user's) serves every worktree, and there\nis no tracked directory to follow."
        },
        {
          "type": "object",
          "properties": {
            "path": {
              "type": "string",
              "description": "The directory git runs the hooks from."
            },
            "outcome": {
              "type": "string",
              "const": "own"
            }
          },
          "required": [
            "outcome",
            "path"
          ],
          "description": "Git already resolves this worktree's hooks to its own directory; nothing was set."
        },
        {
          "type": "object",
          "properties": {
            "path": {
              "type": "string",
              "description": "The directory it now runs the hooks from."
            },
            "enabled": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "description": "Repository settings switched on so that a per-worktree value is honoured, as\n`key=value`; empty when they were on already."
            },
            "outcome": {
              "type": "string",
              "const": "set"
            }
          },
          "required": [
            "outcome",
            "path",
            "enabled"
          ],
          "description": "This worktree's `core.hooksPath` was set, per worktree, to its own directory."
        },
        {
          "type": "object",
          "properties": {
            "path": {
              "type": "string",
              "description": "The directory every worktree runs the hooks from."
            },
            "outcome": {
              "type": "string",
              "const": "shared"
            }
          },
          "required": [
            "outcome",
            "path"
          ],
          "description": "The hooks directory is outside the primary checkout — one directory shared on\npurpose — and every worktree runs it; nothing was set."
        },
        {
          "type": "object",
          "properties": {
            "missing": {
              "type": "string",
              "description": "The directory that would be the worktree's own, and is not there."
            },
            "runs": {
              "type": "string",
              "description": "The directory the worktree runs the hooks from instead."
            },
            "outcome": {
              "type": "string",
              "const": "untracked"
            }
          },
          "required": [
            "outcome",
            "missing",
            "runs"
          ],
          "description": "The primary checkout's hooks directory has no counterpart in this worktree's tree,\nso the worktree keeps running the primary's: pointing it at a directory that does\nnot exist would switch its hooks off."
        },
        {
          "type": "object",
          "properties": {
            "message": {
              "type": "string",
              "description": "What went wrong."
            },
            "outcome": {
              "type": "string",
              "const": "failed"
            }
          },
          "required": [
            "outcome",
            "message"
          ],
          "description": "Git was asked and refused, or could not be run; the message is its own."
        }
      ],
      "description": "What became of the hooks directory of a worktree that was just created, found or moved.\n\nIt travels in the JSON of `worktree create`, `ensure`, `migrate` and `repair`, tagged by\n`outcome`, so a reader branches on one word rather than parsing the line:\n\n```\nuse majordomus_cli::worktree::HooksPath;\n\nlet set = HooksPath::Set {\n    path: \"/r-wt/fix/x/.githooks\".into(),\n    enabled: vec![],\n};\nlet json = serde_json::to_value(&set).unwrap();\nassert_eq!(json[\"outcome\"], \"set\");\nassert_eq!(json[\"path\"], \"/r-wt/fix/x/.githooks\");\nlet back: HooksPath = serde_json::from_value(json).unwrap();\nassert_eq!(back, set);\n```"
    }
  }
}

Examples

These are the capability's own benchmark cases: the same inputs the benchmark runs and the OpenAPI document shows.

default
{}