Skip to content

Worktree Maintenance

Worktree maintenance separates structural consistency repair from policies that remove live worktrees. kwt doctor owns consistency; kwt prune owns explicit live-removal policies.

Inventory boundaries

Git's worktree list --porcelain output is the source of truth per repository. Configured projects, discoverable repositories under the global worktree directory, and existing registry paths provide bounded repository roots; registry metadata and pull-request provenance are auxiliary joins. Registry records with an active creation token are not roots because creation still owns their incomplete state. Duplicate roots that resolve to the same Git common directory produce one repository report.

Linked-worktree administrative directories are accepted only when their gitdir file maps back to that exact worktree. A stale .git backlink to a sibling worktree cannot supply generation or identity state. Repository-level identity comes from the resolved main repository; linked-worktree origins are retained separately for candidate-specific validation.

Inventory roots retain whether they came from configured projects. A configured root requires a canonical configured repository identity; live-origin fallback is reserved for unconfigured global and finalized-registry discovery.

Maintenance discovery is strict: traversal failures and unexpected .git stat or read errors make the inventory incomplete instead of silently hiding a subtree. Every discovered main or linked worktree is inspected as a repository root and deduplicated by its canonical Git common directory. A linked root that Git cannot inspect remains visible as a project_unreachable finding.

Maintenance inventory has two modes:

  • Read-only inspection reads each existing kwt-generation file without creating one. Doctor and prune preflight use this mode, so diagnostics and dry-runs do not adopt legacy worktrees as a side effect.
  • Ordinary operational listing may initialize a missing generation. This keeps normal kwt list behavior as the explicit adoption path for a verified live worktree.

The local inspector contains no provider logic. It gathers Git, filesystem, registry, and provenance facts; merged-PR classification is a later provider layer and completes before mutation starts.

Structural repair

Doctor reports stable findings for broken or ambiguous backlinks, missing worktree directories, stale registry entries, missing generations, unreachable projects, repository identity mismatches, and moved or stale project registrations. The default is read-only. The JSON finding codes are broken_worktree_backlink, ambiguous_worktree_backlink, missing_worktree_directory, stale_registry_entry, missing_generation, registry_generation_mismatch, project_unreachable, and repository_identity_mismatch, duplicate_registry_entry, plus project_path_moved, stale_project_registration, and ambiguous_project_relocation for project configuration.

doctor --fix handles only uniquely identified structural state. For each Git common directory it:

  1. acquires that repository's worktree mutation lock;
  2. rejects an active worktree-creation reservation;
  3. revalidates the inspected path, Git directory, backlink, generation, and existence facts;
  4. repairs uniquely owned backlinks only when every backlink Git could change is included in the validated fix scope;
  5. inventories again and verifies that every record native Git pruning could remove is a uniquely fixable record in the expected snapshot;
  6. invokes immediate native Git metadata pruning only when that complete scope remains valid.

Repair must precede pruning: a repaired backlink can make a moved live worktree usable again, while pruning first could discard the administrative record that repair needs. Locks are acquired and released one repository at a time; doctor never holds multiple repository locks.

After mutation, doctor rescans and compares stable finding code/path pairs with the pre-fix report. Human output lists resolved findings under Fixed before the final inventory; JSON carries them in fixed and counts them in summary.fixed_findings. --quiet suppresses the result report without changing exit status and is mutually exclusive with --json.

Git may repair several backlinks in one repository even when given one linked worktree path. Doctor therefore declines backlink repair for that repository if any live repairable backlink is ambiguous or otherwise outside the validated fix scope. This preserves every manual-only backlink unchanged while allowing independent missing-record pruning and registry cleanup to continue.

Registry cleanup follows Git maintenance. Records with a valid generation use generation-guarded removal. A confirmed-absent legacy record without a generation uses full-entry compare-and-delete under the registry lock, so a concurrent replacement is preserved. When a live registry record's generation is missing, doctor --fix adopts the verified Git generation only by compare-and-swap and clears any inherited expiration. A nonempty generation mismatch identifies a possible replacement worktree; it remains a manual conflict so policy metadata cannot transfer across generations. Inspection and repair use the same canonical path identity, so a registry path reached through an existing symlink still reconciles with its live Git worktree. Ambiguous identities stay reported for manual review. Generation adoption rechecks the inspected generation under the repository mutation lock and performs the registry compare-and-swap inside that guard; a replacement becomes a no-op. Registry records are excluded only while another process holds their creation lock. A token left after its owner exits is abandoned state: doctor inspects it and reacquires that path lock before guarded reconciliation or removal. New expiration records store the destination worktree's observed origin as a credential-free canonical repository identity. Relative filesystem remotes are rejected. Doctor accepts that identity when it matches either the configured project or the verified live origin for the recorded worktree path. It normalizes legacy URL values and renders unparseable legacy identities as [redacted] rather than echoing raw remote data. Configured project identities follow the same output boundary: canonical URL forms are rendered as credential-free identities, while malformed values are omitted and reachable projects fall back to their canonical live origin or local identity.

Registry entries are grouped by canonical filesystem path before individual classification. Two raw paths that resolve to the same live worktree produce one duplicate_registry_entry finding instead of being silently treated as healthy. A group containing an actively owned creation token remains deferred until creation finishes, consistent with other registry inspection. Otherwise, the finding is fixable only when every repository value resolves to the same credential-free identity, and branch, hash, main-worktree flag, registration time, expiration, generation, and unreviewed-source policy are equal. Differing or unsafe metadata remains a manual finding.

For a fixable group, inspection carries the complete observed entries as a private repair condition. Registry mutation validates the complete alias group under one registry lock, then replaces it with one unchanged entry. It prefers the path spelling recorded by Git for the live worktree; if no raw entry uses that spelling, it retains the lexicographically first path for deterministic behavior. Any concurrent addition, removal, metadata change, or creation token makes the transaction a no-op. This grouped compare-and-swap is required because ordinary path-based registry operations intentionally reject multiple keys that resolve to the same path.

Project repair starts only after an effective configured path is confirmed absent. A credential-free repository identity may select exactly one visible main repository as a relocation target. Multiple distinct matching clones produce ambiguous_project_relocation and require manual choice; an unsafe path-derived identity remains project_unreachable. When no matching repository is visible, stale_project_registration authorizes removal of the unchanged registration. If the sole matching live repository is already registered, the stale entry is also a stale_project_registration; repair removes the exact unchanged duplicate instead of relocating it onto the live entry and creating two registrations for one path. If multiple missing registrations independently select the same live target, all remain manual ambiguous_project_relocation findings because kwt cannot safely choose which registration metadata should survive.

Inspection keeps the raw project entry from global TOML beside its independently expanded filesystem view. After Git and registry work is complete, --fix rechecks that the old path is still absent and that a relocation target still has the expected canonical main path, common directory, and repository identity. It then performs a full-entry compare-and-swap under the shared global-config writer lock. Relocation changes only the path and canonical repository identity; removal deletes only the exact raw entry. Concurrent edits and duplicate entries are no-ops. Unknown project fields are retained so a newer kwt version can extend the entry without older repair code erasing its metadata. The atomic writer resolves an existing config.toml symlink and replaces its target, preserving the symlink and target permissions. The repaired path is persisted as the verified absolute canonical main-repository path. The same global-config transaction refuses a relocation if any other current project entry already resolves to the target path. This lock-scoped target guard prevents a concurrent registration—or an earlier repair from a stale report—from creating duplicate project entries even when inspection saw only one relocation claimant.

After every fix attempt, doctor reloads global configuration and the registry before rescanning. Its report and exit status therefore describe persisted post-fix state rather than the inspection that authorized the attempt. Human output groups safe repairs before findings that need review, omits healthy repositories and opaque machine evidence, and shortens paths only for display. Versioned JSON retains exact paths and evidence.

Conditional live removal

Expiration and merged-PR decisions are made outside the mutation lock, but a decision never authorizes path-only deletion. The Git removal transaction rechecks its supplied conditions under the repository lock immediately before removal:

  • the durable 32-character generation;
  • expected HEAD when the policy depends on it;
  • canonical repository identity when the policy depends on it;
  • the linked worktree's local branch and exact upstream repository and branch when merged-PR evidence depends on them;
  • cleanliness when required.

The transaction also rejects active creation reservations, main worktrees, and worktrees that Git marks as locked. Dry-runs use the same lock-scoped inventory and precondition validation as removals. A changed condition returns a typed reason such as generation_changed, head_changed, repository_identity_changed, locked_worktree, or dirty_worktree; the worktree remains.

Merged-prune inventory carries the protected credential names derived from the loaded global configuration into the removal transaction. Every Git process that may execute worktree-selected configuration—including inventory, origin and upstream reads, status checks, lock-time revalidation, and removal—runs with kwt's GitHub and fleet credentials removed from its environment.

Generation-less or malformed-generation live worktrees are never automatically removed. --force for expiration policy can relax cleanliness only. Ignored artifacts do not make an expired worktree dirty. Merged-PR policy uses the stricter rule that ignored untracked files are also local data, and it has no force mode. Expiration removal compares the complete observed registry entry while the Git mutation lock is held. Extending or clearing the expiration after inspection preserves the worktree with expiration_policy_changed.

Merged pull-request evidence

Imported worktrees fetch the recorded PR number and require the live workspace path, branch, head SHA, source identity, project identity, canonical main repository path, durable generation when recorded, and repository alias history to match persisted provenance. Workspace and project paths compare by canonical filesystem identity, so an existing symlink alias is equivalent to its resolved path.

Ordinary worktrees derive source repository and branch from the configured Git upstream as observed from the linked worktree, including worktree-specific Git configuration. GitHub resolves the configured base repository before validating imported alias history or looking up pull requests, so repository transfers do not invalidate recorded aliases. It also resolves the observed upstream source repository for provider matching and diagnostic head lookup while preserving the observed identity for lock-time revalidation. GitHub's commit-associated PR endpoint must return exactly one result whose base repository, source repository, source branch, and head SHA match the local facts and whose merged_at value is non-null. Exact PR head SHA survives squash and rebase merges; local git branch --merged ancestry does not. The configured project identity is the PR base even when the checkout's origin points at a fork. The observed origin identity is read separately from each linked worktree, retained apart from the PR base, and revalidated under the repository lock before dry-run approval or removal. For a globally discovered repository with no configured project, each candidate uses its own observed origin as its PR base; sibling worktrees never share a fallback base.

That live-origin fallback is available only when every configured project root was inspected successfully. If any configured root is unavailable, candidates reached solely through global or registry discovery report doctor_required before provider lookup. Healthy configured projects remain eligible. This repository-wide guard is intentionally conservative: local evidence cannot prove that a rediscovered fork is unrelated to the missing configured upstream, so a merged pull request against the fork must not authorize removal. Running kwt doctor --fix to relocate or remove the stale registration restores the fallback on the next complete inspection.

An unavailable or noncanonical origin fails closed before provider lookup. An expiration registry identity may match either this verified live origin or the configured PR base, preserving existing origin-based records in fork clones.

When no exact commit result exists, source-head lookup is diagnostic only. It distinguishes an advanced branch from no associated PR but cannot authorize removal. Closed-but-unmerged PRs, multiple matches, missing upstream identity, and provider failures preserve the worktree with stable reasons.

All candidate lookups finish before the first removal. A failure for one candidate does not erase another candidate's confirmed result. After removal, registry and imported-PR provenance cleanup use their observed generation or complete value. New provenance records include the worktree generation, so a same-path re-import cannot be deleted by cleanup for the removed incarnation; a concurrent replacement produces cleanup_incomplete rather than being overwritten. Provenance-backed pruning requires a current project registration connected through the record's repository alias history. It holds the project lifecycle claim through a credential-stripped, three-state protected-session probe, worktree removal, and provenance cleanup. Live or indeterminate protected endpoints preserve both the worktree and its durable provenance. Every removal runs from the resolved main repository, so deleting a globally discovered linked worktree cannot invalidate the Git context needed to remove later candidates. Fleet state publishes once after any actual removal, never for doctor, dry-runs, or no-ops.

Command migration and statuses

The command split is intentional:

kwt prune                 -> kwt doctor --fix
kwt prune --expired       -> live expiry policy; absent paths point to doctor
kwt prune --merged        -> clean, exact merged-PR policy

Doctor exits 0 when healthy, 1 when findings remain, and 2 when inspection or fixes cannot complete. Prune exits 0 when confirmed candidates are handled or absent, 1 for safety skips or per-candidate failures, and 2 for usage or inventory failures. Both commands provide versioned JSON output. JSON result reports are written to stdout without duplicating human result summaries on stderr.

Prune reports one stable outcome for every considered path. Policy and safety outcomes include removed, would_remove, missing_generation, doctor_required, dirty_worktree, head_advanced_after_pr, no_associated_pr, pr_not_merged, source_repository_unavailable, source_repository_mismatch, source_branch_mismatch, ambiguous_pr_match, authentication_failed, network_failure, generation_changed, expiration_policy_changed, head_changed, repository_identity_changed, main_worktree, locked_worktree, protected_session_live, protected_endpoint_inventory_incomplete, registration_changed, and cleanup_incomplete. no_associated_pr and pr_not_merged are normal non-candidates and do not by themselves make prune exit nonzero. An individually discovered path that cannot be inspected becomes a doctor_required outcome while other repositories continue. A strict discovery error still aborts with exit 2 because the inventory is incomplete. A Git removal that deregisters the worktree but leaves residual files completes guarded registry and provenance bookkeeping, then reports cleanup_incomplete with the residual path rather than treating the mutation as wholly failed. Multiple registry entries that canonicalize to one candidate path also produce doctor_required; merged pruning requires one unique registry snapshot before it can authorize or clean up that worktree.