Skip to content

Latest commit

 

History

History
632 lines (589 loc) · 48.9 KB

File metadata and controls

632 lines (589 loc) · 48.9 KB

Worktree Manager Spec

wtt is a Git worktree manager.

Initial platform support is Linux and macOS. Windows is out of scope until equivalent process-tree cancellation and shell integration are specified.

Contract conventions

Authority

SPEC.md ──delegates named subject detail──> docs/
PLAN.md      delivery scope and order
AGENTS.md    engineering constraints
ISSUES.md    unresolved verified defects and decisions

The subject owners are indexed in docs/README.md. On conflict this file wins; its silence delegates only the indexed subject, never retires a root invariant. Archived documents have no authority.

Provider state vocabulary

Axis State Authority and consequence
Knowledge unknown / not_attempted None; infer neither presence nor absence.
Knowledge unavailable / failed attempt Retain last-known facts as stale; never infer absence.
Knowledge absent Current authoritative absence; no PR facets; may satisfy no-PR selection.
Knowledge present Current authoritative PR identity with independently authoritative facets.
Knowledge not_applicable Provider authority is structurally unnecessary; use local rules.
Coordination queued / deferred Retain cached facts; grant no provider authority and show no execution.
Coordination executing Retain cached facts; may mark target-specific execution.
Coordination complete Work is terminal; consult knowledge and facet authority for its result.

Freshness is a third independent axis: stale facts may remain visible but cannot satisfy an operation requiring current authority. Human and JSON output preserve every axis.

Mutation and publication boundary

Every cancellable mutation or publication follows this sequence unless a narrower section names a different boundary:

collect → validate → [final cancellation check] → MUTATE / COMMIT → reconcile / render
                   cancellation wins ← | → completion wins
                                         ^
                               linearization point

Cancellation before the boundary preserves prior authoritative state and output. After the boundary, completion wins; failure in later bookkeeping preserves the successful external operation and reports recovery instead of attempting an unsafe rollback.

Features

Base

  • Index Git repositories in the filesystem. The user config defines one or more scan roots.
    • Scan roots are searched recursively. By default, the scanner does not follow symlinks or cross filesystem boundaries.
    • Do not descend into a repository after Git validates its boundary, or into built-in exclusions for common tool and package-manager caches. A .git marker alone is not sufficient: if Git rejects it, report a warning and continue looking for nested repositories. Users can add exclusion globs.
    • Yield directory entries named .git for boundary validation because a scan root may contain a canonical bare or separate common Git directory with that name; never descend inside such an entry after it is processed.
    • Permission and transient filesystem errors do not abort a scan; report them as warnings.
    • A repository found under a scan root is in scope. Once found, use git worktree list --porcelain to index all of its worktrees, including worktrees whose paths are outside every scan root.
    • Canonicalise repositories by their common Git directory so encountering a linked worktree does not create a duplicate repository entry. If canonicalisation fails, warn and do not store a non-canonical fallback identity.
    • Recognise both files-backed and reftable Git directory layouts as candidates; Git remains the authority that validates whether a candidate is a repository boundary. A candidate that is itself the canonical common Git directory is in scope even when it belongs to a non-bare repository with a separate external worktree.
    • Reconcile clean scans into stable repository/worktree identities so cached provider state and hook trust survive. A clean scan removes entries no longer present. If any filesystem or Git warning makes a scan partial, update successfully discovered repositories but retain previously indexed repositories that were not observed; a later clean scan may remove them. Partial retention reads and writes occur under the same immediate transaction, so a concurrent completed refresh cannot be overwritten by a stale read/modify/write merge. Discovery and reconciliation are serialized per index across processes so an older overlapping discovery cannot overwrite a newer completed refresh. Retained-only repositories preserve their previous repository-level scanned_at; only successfully observed repositories receive the new scan time. The refresh lock is derived from the canonical index path so symlink aliases serialize on the same lock identity.
    • SIGINT/SIGTERM and internal cancellation abort discovery, Git inspection, and index reconciliation without replacing the last complete index. Git subprocesses run in isolated process groups; cancellation and timeout terminate and reap the direct child and kill its process group, including descendants that outlive the direct parent. A descendant that deliberately escapes that group is outside the containment guarantee, but must not delay command timeout or cancellation by retaining an inherited output pipe. Reconciliation's final cancellation check is the shared commit linearization point.
    • Store the index in SQLite WAL mode so refresh writes can commit while interactive readers retain a consistent snapshot. Index reads and partial-scan merging are cancellation-aware.
    • Scanner-owned Git commands ignore inherited repository-selection and discovery environment such as GIT_DIR, GIT_WORK_TREE, and GIT_COMMON_DIR; each candidate path is authoritative. Transient GIT_CONFIG_COUNT/GIT_CONFIG_PARAMETERS injection is also cleared, while ordinary protected system/global configuration remains in force. Recognized bare candidates are explicitly selected by canonical absolute --git-dir, preserving compatibility with protected safe.bareRepository=explicit policy and relative scan roots.
    • The indexing daemon defined below alone performs filesystem-wide discovery and background indexing. Commands that successfully mutate Git may reconcile only the affected repositories so their results are reflected immediately without depending on the daemon.
    • Nonempty stderr from a successful scanner-owned Git command is retained as a Git warning, even if a later inspection step fails. On complete inspection the repository remains discovered, but reconciliation treats that scan as partial.

Indexing daemon

  • Default paths follow the XDG Base Directory specification on both Linux and macOS. Configuration is $XDG_CONFIG_HOME/wtt/config.toml (falling back to $HOME/.config/wtt/config.toml) and rebuildable state is under $XDG_STATE_HOME/wtt (falling back to $HOME/.local/state/wtt). Relative XDG base directory values are ignored as the XDG specification requires. Explicit --config and --database paths take precedence.
  • The first non-metadata command atomically creates a default configuration with no scan roots when the XDG configuration file is absent. It never overwrites a file; a missing explicit path is an error.
  • wtt daemon start atomically acquires lifetime ownership of the canonical index, watches configured roots, and performs incremental scans plus periodic full scans for initialization, validation, and missed events. A second daemon fails. On supported local filesystems, the default watcher uses the platform's native event facility and does not implement watching by repeatedly traversing configured roots. Native watcher registration failure or resource exhaustion degrades affected coverage to MAYBE_STALE; periodic validation repairs missed events without replacing the watcher with continuous full-tree polling. Missing or prunable worktrees remain indexed but their absent paths are never registered. An absent dynamic path becomes eligible again only when repository topology changes or the path reappears. Filesystem bursts are filtered and coalesced by repository before bounded incremental admission so status IPC remains responsive while scan and provider workers are busy. --detach backgrounds it and returns when ownership, the index, and status IPC are ready—not when indexing finishes. With --detach --if-needed, a running daemon is a successful no-op; --if-needed is otherwise invalid.
  • wtt daemon status separately reports reachability, index operability, watcher coverage, snapshot freshness and validation, scan progress, recent warnings, and the log path.
  • wtt daemon stop requests clean shutdown. Direct wtt daemon, lifecycle flags on daemon, shutdown, and run are not public spellings. start runs in the foreground unless --detach is present; --if-needed is valid only with start --detach. The current CLI may use the unchanged authenticated shutdown exchange from exactly the immediately preceding daemon protocol to stop an older same-index owner cleanly. When daemon auto-start or detached start is requested, it waits for that owner to release authority before starting the current daemon; all non-lifecycle requests remain fail-closed across protocol versions.
  • wtt scan queues a full local scan and returns. --wait follows the queued or coalesced generation and reports its completion and warnings. Neither blocks snapshot readers, and cancelling a waiter does not cancel shared daemon work.
  • wtt scan --refresh additionally submits one daemon-owned provider phase for the same global index. Local and provider admission, progress, authority, and terminal outcomes remain independent. With --wait, both requested phases are followed and reported separately; failure of either requested phase returns nonzero without undoing a successful phase. The public scan --provider spelling is removed completely.
  • Provider synchronization also has a first-class wtt refresh command. Bare refresh targets the current worktree; refresh WORKTREE targets one exact logical worktree or indexed absolute path; refresh --repository targets every provider-capable worktree in the inferred current repository; and refresh --all follows one durable complete-index convergence sweep. These scopes are mutually exclusive. Explicit refresh waits for its terminal report by default and preserves typed per-target outcomes without exposing internal execution windows as request boundaries.
  • Scans publish validated incremental results rather than replacing the index at completion. Every write advances a monotonic revision; older work must merge, retry, or fail, never revert newer facts. A generation is complete only after covering every root and replaying events observed during it.
  • Read-dependent commands consume the latest snapshot, including partial initial results, and expose progress, freshness, and an initial-index warning. Client auto-start defaults on. If auto-start fails or is disabled, daemon absence, watcher loss, or incomplete coverage marks results MAYBE_STALE and suggests wtt daemon start --detach --if-needed. Corruption, incompatibility, migration, permission failure, and uncertain ownership are errors. Metadata-only commands remain daemon-independent.
  • Successful Git mutations directly reconcile affected repositories. Reconciliation failure preserves the Git result and marks the index stale; background work cannot overwrite a newer reconciliation.
  • Restart serves the last snapshot as pending validation while validating it in the background; provider and local-index authority remain independent.
  • Configuration changes are debounced and applied atomically after validation. Invalid changes retain the prior configuration and warn; root changes schedule reconciliation.
  • Each watcher generation resolves a configured root to its current canonical target. A rename, deletion, recreation, mount replacement, or symlink-target change creates an observation gap: affected coverage is MAYBE_STALE until watchers are rebuilt and a full validation of the new target commits. Overlapping roots are union coverage; removing one root never removes a repository still reached through another. Platform watcher differences may delay detection until periodic validation but may never preserve authoritative coverage across an observed gap.
  • Operational logs are private, redacted, size-rotated, and retained within a bound in the application state directory. Foreground diagnostics may also use stderr, never stdout.

Configuration and installation diagnostics

  • Fatal human CLI diagnostics keep the failed condition, causal chain, and recovery guidance distinct. The primary message identifies the operation and concrete condition; a stable code identifies the leaf condition; and an optional help: section contains at most one preferred deterministic next action (or a short ordered list when the alternatives materially differ). Unknown I/O, Git, protocol, clock, cancellation, and internal failures receive no speculative hint. Diagnostics and user-controlled values are terminal-safe and bounded. A command is rendered only when every interpolated value is safely representable by the supported shell boundary; otherwise the exact value is presented separately with a placeholder command. Human hints remain on stderr and never enter JSON stdout, completion output, private switch records, or another machine protocol.

  • Recovery facts belong to the application layer and remain independent of interface presentation. The CLI projects facts to command-oriented help: text, while Mission may project the same facts to its typed SuggestedAction vocabulary. Neither interface derives scope, mutation safety, collision, freshness, or retry policy independently, and neither presentation type is translated into the other.

  • wtt config owns typed configuration inspection and mutation through show, get KEY, set KEY VALUE, path, validate [PATH], and reference. show prints the effective validated configuration, path prints the active path, and reference prints every supported key and default with concise comments. get and set use stable dotted keys.

  • config set performs one atomic read, typed update, whole-document validation, and replacement. An unknown key, invalid value, malformed existing file, or failed replacement leaves the prior bytes unchanged. Configuration output contains no secret value unless that field has an explicit disclosure contract.

  • One statically defined typed schema is the sole inventory for stable dotted keys, defaults, validation metadata, reference output, and finite completion values. Boolean values complete as exactly true and false; public enums complete as exactly their accepted variants; numeric, path, glob, and binding values have no fabricated finite candidates. Schema completion is pure and remains available without reading or creating configuration or index state. Every tui.bindings.<action> leaf is generated from the canonical application-action registry.

  • The pre-release provider configuration exposes intent rather than scheduler internals. provider.automatic_refresh defaults to true and controls only unattended daemon refresh; TUI-visible and explicit refresh remain available when it is false. provider.background_refresh_seconds defaults to 300 seconds for unattended active work. The application derives pending-check refresh at 30 seconds and recent-terminal core refresh at 3,600 seconds, with bounded absence and failure retry schedules. Closed PRs use that terminal cadence only through the two-day stabilization window defined by the provider convergence contract; stabilized terminal facets do not recur merely because core membership is refreshed. TUI-visible refresh uses a fixed, non-configurable 15-second heartbeat. provider.execution_window_targets defaults to 512 and bounds active provider target operations without limiting accepted refresh scope. Subprocess timeout, batch, concurrency, output, daemon validation, and log bounds remain advanced safety controls.

  • The removed provider mode, payload TTL, scheduling TTL, retry-backoff, and relevant-elapsed keys have no aliases, deprecation parser, or compatibility shim. They are unknown keys and fail with an actionable validation error. Automatically created configuration and config reference use the canonical schema, distinguish normal from advanced settings, and include concise section guidance and defaults.

  • wtt doctor is read-only. It reports stable check identifiers and ok, warning, failed, or unavailable status for configuration, scan roots, index compatibility, daemon/watcher state, Git, optional provider tooling, determinable shell integration, and configuration/state/database/ log paths. It never repairs state. --provider opts into bounded provider authentication/network checks; without it, provider diagnosis is local-tooling-only. --json emits a versioned object and human prose never enters structured stdout.

  • A future index schema requires upgrading WTT and is never presented as rebuildable by an older binary. For a corrupt, unversioned, or otherwise incompatible rebuildable index, recovery is explicit and non-destructive: identify the database and state paths with wtt doctor, stop the owning daemon, copy the complete state directory to a backup, move the database and any matching SQLite sidecars into a separate retained backup location, then start the daemon and run wtt scan --wait to rebuild from Git and filesystem authority. WTT never deletes, overwrites, or automatically replaces the failed index. Custom --database paths use the same procedure at the reported location.

  • Help groups arguments, command-specific options, scope options, output options, then global options. Conventional short flags are deliberately limited to -a/--all for list and switch, -f/--filter, -j/--json, -n/--dry-run, -y/--yes, -w/--wait, -c/--create, and -b/--base, plus existing -v, -h, and -V. Safety overrides and --all-repositories remain long-only.

  • Every command whose primary result is meaningful state or a terminal operation report supports a versioned --json document. Human diagnostics and progress never contaminate structured stdout.

Listing and discovery

  • wtt list is the only non-interactive discovery command. The public wtt search spelling is removed. wtt switch without a target owns interactive fuzzy discovery.

  • wtt list [--filter QUERY] reads the committed daemon-maintained index without starting a scan. Its human default is a headed responsive table led by REPOSITORY, WORKTREE, BRANCH, LOCAL, PR, and meaningful AGE; primary worktree identity is never removed at narrow widths. Missing, unavailable, stale, unknown, and not-applicable values remain distinct. Redirected human output uses a deterministic non-terminal width; literal delimiter output, if retained, requires --tsv. --verbose adds operational detail and exact timestamps without changing row identity or order.

  • --filter uses the shared grammar and values in the delegated query contract:

    query     := term { whitespace term }
    term      := fuzzy | qualifier ":" value
    qualifier := repo | wt | branch | state | pr | repo-pr | ci | fresh | hide
    value     := exact | quoted-exact | exact "*"
    

    Repeated values of one qualifier are ORed; different qualifiers, flags, and --filter are ANDed. repo-pr:open admits repositories with a known nonterminal PR; open-or-unknown also admits those without membership authority. pr:open filters rows; hide:quiet-main suppresses clean main rows with known non-open state. Unknown grammar or values are errors. CLI, picker, and Mission share one Unicode-aware parser, evaluator, ranker, and validator; typed case breaks only otherwise-equal ties. Non-interactive identity values are exact unless suffixed by *.

  • --dirty and --clean are mutually exclusive local-state filters; with neither, dirtiness does not filter. Query-time provider policy is --provider-refresh never|needed|all: never reads committed cache only, needed submits demand-aware eligible work, and all follows complete convergence for the promised query scope.

  • Repository-aware commands use this effective scope:

    Invocation context list / switch default prune default Explicit global scope
    Non-bare repository Canonical current repository Canonical current repository --all / --all-repositories
    Outside Git Global Error --all / required --all-repositories
    Bare repository Global Error --all / required --all-repositories

    Filters only narrow this scope. Human results and plans state inferred repository scope once; interactive headers always show scope; JSON includes typed effective scope. Notices never enter JSON or machine-protocol stdout.

  • list retains repeatable structured filter flags and grouping/sorting conveniences only by compiling them into shared application models. It defaults to repository grouping and stable repository/worktree order; fuzzy text uses relevance with deterministic identity ties unless an explicit sort replaces rank.

  • --json remains the authoritative versioned byte-safe interface. It contains typed effective scope and a worktrees array; paths retain lossy display and exact Unix raw_base64. Human grouping, table decoration, and scope prose are absent.

  • Interactive switch discovery is cache-only. Opening, editing, navigating, resizing, or toggling scope submits no provider request. Cached annotations remain visible and searchable, historical provider failures are not replayed as current warnings, and transient daemon readiness loss retains the last committed picker rows with one availability warning.

  • Generated Bash, Zsh, and Fish completions use a bounded quiet cache-only typed boundary. They do not start the daemon, invoke Git or provider tools, scan, access the network, or fall back to filesystem names. Switch completion reuses effective scope, live-target eligibility, and empty- query ordering. A unique logical name is inserted in effective scope; ambiguous global names use an absolute path accepted by switch. Values unsafe for the active shell are omitted. Busy, absent, incompatible, or unavailable cache state returns no candidates and no prompt diagnostic.

  • Repository reconciliation indexes every local branch and its attached worktree, if any, as rebuildable completion inventory. create NAME --branch and switch -c NAME --branch complete exact unoccupied local branches from that cache. Ordinary switch completion remains limited to existing worktrees and never implies mutation. Final creation revalidates branch existence, occupancy, base/HEAD authority, and destination collisions against live Git and the filesystem.

  • Generated adapters obtain configuration keys and finite values only from the canonical typed schema. config get and the key position of config set complete every stable leaf; config set value completion is offered only for a schema-declared finite vocabulary. Selecting a typed completion boundary suppresses filesystem fallback even when that boundary returns no candidates. Completion never reads or repairs mutable configuration to discover the schema.

  • Dynamic completion respects the real shell grammar. Zsh uses its CURRENT word index. switch and remove offer a target only while their single positional slot is open, including after --, and do not mistake option values for targets. switch --create does not offer existing targets. Repeatable filter options remain repeatable. Bash, Zsh, and Fish implement equivalent position policy and never fall back to filesystem candidates at typed boundaries.

Worktree switching and creation

  • In an interactive terminal, omitting the subcommand opens the same cache-only picker as wtt switch without a target. Switch-compatible global options may precede the omitted command. --help and --version retain root metadata behavior; another root operand remains an unknown subcommand rather than an implicit target; root completion continues to offer commands and options. Bare invocation with redirected stdin or stdout prints root usage and fails without opening a picker or waiting for input.

  • Generated shell wrappers route effectively bare interactive invocation through the switch handoff. Opening or cancelling performs no mutation, scan, provider request, Git call, or network access. Only bare entry adds muted wtt tui and wtt --help hints above search. The header gives the cursor-scrolling query at most 30 cells, then shows scope and its Ctrl-R destination. Narrow layouts compact scope before shrinking the cursor-visible query viewport; query state is never truncated. Onboarding may displace one result row, never guidance or diagnostics.

  • A worktree's logical name is the main worktree's directory name, the suffix after {MAIN_WORKTREE_NAME}. for a managed child, or otherwise the linked worktree path's final component.

  • Switch with wtt switch {WORKTREE_NAME_OR_PATH}.

    • With no target, open the shared interactive picker.
    • Use the shared effective scope above; interactive Ctrl-R toggles repository/global scope.
    • Global exact-name resolution succeeds only when the name identifies exactly one worktree. On ambiguity, fail and list each candidate with its repository and absolute path. An absolute indexed path is also an exact public target so dynamic completion can disambiguate duplicate names. Resolution never applies fuzzy or suffix matching.
    • An exact miss remains non-mutating and uses application-owned recovery facts in this precedence: an existing branch-normalized logical name; one or more matching targets outside inferred repository scope; the owning worktree of an already attached exact branch; an unoccupied exact branch; viable explicit creation; then a known collision or missing creation context. A unique out-of-scope target suggests wtt switch --all; multiple targets list deterministic repository and absolute-path identities. An attached branch never suggests duplicate creation. An unoccupied slash-containing branch suggests wtt switch -c BRANCH; default creation both reuses that branch and derives the separator-free logical name. A generic miss offers creation only after live Git and filesystem preflight proves the derived identity available. Otherwise it explains the known constraint and points to wtt create --help. Discovery of recovery facts does not change exact resolution, and cached scope facts are revalidated by the normal switch path when selected.
    • Detached worktrees are valid switch destinations. Before returning a destination, verify directly that the repository-qualified indexed worktree still exists. Missing or otherwise invalid paths are not selectable switch targets, and the selected target is revalidated immediately before its private path record is emitted. Switching to the current worktree succeeds.
    • Completing a switch requires generated Bash, Zsh, or Fish integration because a child process cannot change its parent shell's directory. A direct invocation may still open and operate the interactive picker for discovery. Cancelling is quiet; accepting a valid destination emits one actionable integration diagnostic on stderr, writes nothing to stdout, performs no selected-path provider bookkeeping, and returns nonzero. Direct exact-name resolution follows the same completion behavior after verifying the destination.
    • The private wrapper record carries the exact destination and prevalidated repository identity; post-cd acknowledgement cannot reclassify a completed switch after a pathname race. Human diagnostics never expose its framing or present stdout as shell integration.
    • Parent-shell cd is the switch linearization point. Before it, the original directory and recency remain unchanged; after it, success wins and one synchronous acknowledgement allocates a globally monotonic MRU sequence. A vanished target produces a contextual wtt switch diagnostic without wrapper internals; cleanup is silent under interactive job control.
    • For an empty interactive query, valid non-current destinations rank before the current worktree, then by successful-switch recency, meaningful worktree activity, and deterministic repository- qualified identity. Never-switched destinations use meaningful activity, not scan, refresh, or provider-attempt time. The initial recency facts remain frozen while the picker is open so row replacement preserves selection and viewport.
    • For a nonempty query, relevance classes are exact repository, exact logical worktree, exact branch, repository prefix, logical-worktree prefix, branch prefix, then fuzzy projection match. A candidate takes its strongest class. Shared application query code normalizes relevance to 0..=1000 and forms anchored near-tie bands no wider than 20 points from each band's highest remaining score. Successful-switch recency and meaningful activity order only within one class and band; neither may cross those boundaries.
  • wtt create and wtt switch --create (switch -c) share these creation modes:

    Mode Branch requirement Start point Result
    Default, derived branch absent Exact derived local branch absent --base REF or current HEAD Create derived branch and worktree.
    Default, derived branch present Exact local branch unoccupied; no --base Existing branch Reuse branch and report reuse.
    --new-branch BRANCH Named local branch absent --base REF or current HEAD Create named branch and worktree.
    --branch BRANCH Named local branch exists Existing branch; --base invalid Create worktree; Git decides occupancy.
    --detach REF Not applicable Resolved REF; --base invalid Create detached worktree.

    An occupied default branch fails with its owning path; an existing default branch plus --base fails; remote-tracking branches are never promoted. wtt switch BRANCH without --create remains non-mutating and suggests creation for an exact unoccupied local branch. switch --create accepts every mode and uses the same planner, collision, hook, reconciliation, and recovery policy as standalone creation. Standalone creation never changes the parent shell; switch creation requires shell integration before mutation and enters the result after success.

    • The first operand is the worktree-name seed. Replace / with - for derived logical identity and sibling placement, then reject empty, ., .., path separators, NUL, or a collision with any logical name, branch-derived name, registered worktree, or filesystem entry; never append an implicit suffix.
    • Validate new branch names and resolve every start point before mutation. In explicit --branch and --detach modes, the first operand remains only the worktree-name seed.
    • The repository's main worktree, in Git's terminology, is the parent for placement. For main worktree {ROOT_DIR}/{MAIN_WORKTREE_NAME}, children are created as {ROOT_DIR}/{MAIN_WORKTREE_NAME}.{NEW_WORKTREE_NAME} even when creation starts in a linked worktree. Bare repositories cannot create managed worktrees through this command initially.
    • On success, reconcile the affected repository and print the destination path. Standalone human output describes the created identity; --json is versioned and byte-safe. If only reconciliation fails, preserve the successfully created worktree, report a warning, and instruct the user to run wtt scan.

Worktree removal and pruning

  • Remove with wtt remove {WORKTREE_NAME}. Both remove and prune accept --dry-run; dry run executes the real planner and current validation path, performs no mutation or confirmation, and renders the same candidates, safety facts, selection reasons, refusals, waived invariants, and branch disposition as execution. --dry-run --json emits a versioned advisory plan; later execution always recollects and revalidates mutable facts.

  • Prune requires one or more explicit selection flags: --merged-pr selects authoritative merged pull requests; --merged-into[=BRANCH] selects candidate HEADs that Git proves are ancestors of an exact local branch; --include-unmerged selects attached branches for which both integration routes are decisively false; and --include-detached selects locally safe detached or branchless worktrees. Bare prune is an error, removed pre-release --merged and --no-pr spellings are not aliases, and selectors form a rendered union while machine output preserves every established typed reason.

    • An omitted merge target resolves independently to each repository's structural main-worktree branch. An explicit target resolves only refs/heads/<BRANCH>; tags, remote-tracking branches, object IDs, revision expressions, invoking linked-worktree branches, forge defaults, and a literal fallback such as main are never substituted. A missing or detached default target, or absent explicit target, aborts the complete assessment. The target worktree is always excluded.
    • Git ancestry is a typed positive, negative, or failed assessment equivalent to git merge-base --is-ancestor C T. Unmerged classification requires decisive non-ancestry and current authoritative provider state that is not merged; open, closed-unmerged, and absent all qualify. Unavailable, failed, stale, cancelled, or unattempted authority is unknown, never a negative fact.
    • Use the shared effective-scope table. Canonical common-directory identity makes main and linked invocation equivalent. Repository scope includes an otherwise unindexed current repository; all-repository scope includes the index and first reconciles an unindexed current repository. One invocation assesses every eligible worktree in scope, not one selected branch.
  • Removal and pruning safety:

    Condition Default decision Sole override / consequence
    Main worktree Refuse remove and prune. None.
    Dirty Refuse. --allow-dirty; waive only dirtiness.
    Locked Refuse. --allow-locked; waive only the lock.
    Attached branch has no upstream or unpushed commits Refuse provider-only or unmerged prune; exact ancestry supersedes this requirement. --allow-unpushed; retain branch.
    Detached or branchless Exclude from prune. --include-detached; provider/upstream become not_applicable.
    Detached HEAD unreachable from a local branch, remote-tracking ref, or tag Refuse. --allow-unreferenced, valid only with --include-detached; warn about garbage collection.
    A remaining provider-dependent candidate lacks current authoritative presence/absence Cannot satisfy selection or unmerged classification. None; abort incomplete assessment.

    Named overrides are independent and never imply one another.

    • Provider routing is selector-aware. Ancestry-only prune performs no provider work, and an ancestry-selected candidate in a mixed union needs no provider authority. Prune freezes only remaining provider-dependent targets, submits one durable scoped refresh, and waits through admission and execution windows. It plans only after every frozen incarnation has sufficient current authority. Failure, cancellation, or obsolete identity aborts without a plan, prompt, mutation, or silent retry.
    • Show every proposed removal, including its safety state and reason for selection, before making changes. Interactive sessions require confirmation; non-interactive sessions require --yes and never assume confirmation. --yes skips only confirmation and grants no safety override. Human output groups eligible removals and decisive exclusions, suppresses irrelevant cascading reasons, uses PR: none for authoritative absence and PR: n/a for non-applicability, and ends with exact eligible/excluded/branch-deletion counts. Versioned JSON preserves the same typed applicability, authority, selection, reachability, and terminal assessment state.
    • After confirmation, attempt every eligible removal even if an earlier removal fails. Report removed, skipped, and failed worktrees separately; each skipped or failed target names its exact reason, including dirty or locked state.
    • Perform removals through git worktree remove; never delete a worktree directory directly.
    • An unmerged or detached removal always retains its branch. After safely removing a clean, non-overridden worktree selected by merged-PR or ancestry proof, delete its local branch through expected-ref guards. Execution revalidates candidate identity, exact target identity and tip, and ancestry immediately before removal; ancestry-based branch deletion atomically verifies the target ref again. If removal succeeds but that guard fails, retain the branch and report the partial outcome. The public --force spelling remains removed.
    • Track and display Git's stale or prunable worktree metadata separately. Stale metadata alone never causes wtt remove or wtt prune to remove a worktree.
  • Provider status initially supports GitHub through authenticated gh. Resolve each attached branch's effective push repository using Git push-remote precedence and pushurl, preserving fork identity; branched non-bare main worktrees are normal candidates. Detached heads and repositories without a GitHub remote are not_applicable. Missing or unauthenticated gh, rate limits, and network failure are unavailable and never disable local work.

  • Discover at most 16 admitted targets per GitHub host in one lightweight batch, publish exact core PR membership independently, and enrich only the selected PR's due facets through a separately bounded query. Ambiguous, paginated, identity-less, or incompatible responses use a bounded compatibility path and cannot establish false absence or revoke current authority. Draft PRs count as open; choose the most recently created open PR, otherwise the latest merged or closed PR.

  • Provider work is daemon-owned. Clients submit typed due, force, complete, or forced-scoped demand and never collect directly as a fallback. Valid demand is coalesced or durably queued under pressure; the default 512-target execution window bounds active work, not accepted scope. A complete refresh freezes one capable target set and converges across bounded, independently published checkpoints. Compatible requests follow existing work, while changed incarnations are reconciled without overlapping full sweeps.

  • Core membership, checks, diff/detail, and review-thread facets have independent freshness, demand, failure, and publication authority. Absence creates no enrichment; stabilized terminal facets do not recur automatically; optional failure cannot erase current membership. Human rows preserve stable facts and append at most , !, or ~ for executing, latest failure, or stale, in that precedence.

  • Provider subprocesses, batches, turns, concurrency, output, IPC frames, and response writes are independently bounded. Operational warnings name the failed phase and report bounded per-turn and cumulative progress; queued or unstarted work is never mislabeled as a provider timeout. Clients receive one complete frame or a typed transport failure, never partial JSON.

Hooks

  • Repository setup hooks live under .wtt/hooks/.
    • wtt hooks install installs hooks under <git-common-dir>/wtt-hooks, where <git-common-dir> is returned by git rev-parse --git-common-dir. All linked worktrees therefore read and write the same installed hooks.
    • Repository hooks are untrusted by default. Before first execution, show the commands/files that will run and require interactive approval for that repository. Record trust against both the repository identity and a digest of the installed hook contents; changed hooks require approval again. wtt hooks trust and wtt hooks revoke manage this decision explicitly.
    • Never prompt or execute untrusted hooks in a non-interactive session; fail the hook phase with an actionable message. A hook failure stops subsequent hooks and makes the parent operation fail, while preserving any worktree Git already created and reporting its path for recovery.
    • The initial post-create event consists of regular executable files directly beneath .wtt/hooks/post-create. Installation rejects symlinks, directories, and non-executable files, copies the set into the common Git directory, and executes the installed files directly in bytewise lexical path order with the new worktree as the working directory. Hooks receive WTT_EVENT, WTT_REPOSITORY, WTT_WORKTREE, and WTT_BRANCH; each hook is bounded by hooks.timeout_seconds, and timeout or cancellation terminates its process group.

Shell integration

  • Bash, Zsh, and Fish are supported.
    • Completions for the CLI.
    • wtt shell init {bash|zsh|fish} emits a wtt wrapper/function for users to source or evaluate.
    • The wrapper changes its directory for a successful wtt switch destination, including switch --create; standalone create never does. Without integration, switch acceptance or exact resolution fails with installation guidance, and switch-create fails before mutation.

Interface

wtt tui opens Mission, the single global repository/worktree inventory.

  • Mission always retains the complete indexed inventory as its source. Repository focus, all-worktrees mode, grouping, collapse, and search are reversible projections and cannot discard query state or repository-qualified selection while it remains eligible. The effective projection is visible and bounds rendered rows, local-diff work, and provider interest.
  • A new session visibly applies repo-pr:open-or-unknown hide:quiet-main. The first predicate admits repositories with a last-known nonterminal PR or without usable membership authority; the second hides only clean main worktrees with known non-open state. Stale facts retain their lifecycle and marker under the shared state vocabulary. Removing either predicate broadens only its dimension; Show all or clearing both reveals the full inventory.
  • Mission defaults to repository then PR/worktree-status grouping and meaningful Updated order. Updated is the newest PR update or durable local HEAD/branch change, with canonical identity ties; scan, cache, attempt, visibility, selection, scheduling, and completion times never count. Background replacement preserves existing identity order. Explicit sorting may recompute it, and an explicit refresh may do so once only when meaningful facts changed.
  • Stable provider knowledge and lifecycle alone determine provider grouping, sorting, and filtering. Provider-incapable worktrees use N/A independently from local missing/prunable state. Every leaf group is collapsible; a collapsed bar is a typed selectable target and cannot leak a hidden child into actions or provider interest. Collapse preferences and child restoration survive projection changes, while search may reveal children without mutating those preferences.
  • Rows keep logical worktree and branch identity separate and expose color-independent local flags, PR lifecycle and number, CI summary, diff, review-thread counts, and meaningful age. Shared states and valid-empty values remain distinct. Narrow layouts remove fields by information priority and may expand only the selected logical row; both physical lines retain one selection, mouse target, hyperlink projection, and provider identity. Monochrome retains every semantic distinction.
  • The viewport moves only enough to reveal the complete selected logical target and backfills at the dataset end. Group bars, pinned context, and responsive detail count as physical lines. Projection, resize, snapshot, and overlay changes preserve canonical focus and the smallest valid displacement; gg resets focus and viewport to the absolute origin. Empty index, preset-hidden inventory, and user-query misses remain distinct and provide recovery appropriate to the affected layer.
  • Known PRs display #<number> in CLI, picker, and Mission rows. A canonical provider URL becomes an OSC 8 hyperlink only on a supporting terminal; redirected output and --no-color retain identical plain text, and structured JSON is unchanged.
  • One declarative application-action registry owns each public action's stable identifier, tui.bindings.<action> key, defaults, label, description, target requirements, availability, and typed dispatch. Commands, configuration reference, and binding lookup derive from it. Unknown or conflicting configured bindings are diagnosed and fall back safely; unbound or unavailable actions remain discoverable with reasons. open_commands defaults to both ? and Ctrl-P unless explicitly replaced.
  • Single-line editors own every unmodified printable character. Only cancellation, acceptance, navigation, the surface's documented completion action, and established readline editing keys bypass text insertion. Ctrl-W and Ctrl-Backspace use readline word-rubout, treating : as the structured-token boundary. Terminal decoration cannot displace the visible cursor from the editor's logical position.
  • / edits the shared listing query. Invalid drafts retain the last valid projection and show a terminal-safe error. Interactive prefix suggestions are cache-only; completion keys alone materialize an exact quoted candidate and cycle a frozen candidate set. Enter accepts the query or selected row without silently rewriting it. Search is Unicode-aware case-insensitive, with typed-case agreement only an otherwise-equal tie-breaker.
  • Discrete selectable lists use cyclic semantic previous/next navigation, skipping structural and hidden rows and remaining within the active pane or overlay. Empty and singleton lists are stable; paging, mouse scrolling, editor cursors, and scroll-only text remain bounded.
  • Enter on a worktree opens a bounded contextual action popup whose initial facts come from cache. Optional last-commit detail loads asynchronously through an identity- and HEAD-qualified request; cancellation or obsolete completion cannot replace current state. Initial typed actions are Switch here, Open pull request, Copy path, Reveal in files, Refresh current, Remove, and repository Prune. Renderers perform no I/O, bounded adapters never interpolate shell commands, and failure preserves the popup with one typed diagnostic.
  • Switch here restores the terminal before the shared final boundary and private wrapper response; direct invocation keeps it disabled. Copy path uses bounded OSC 52 with exact Unix path bytes, Open pull request uses only the cached canonical URL, and Reveal uses the supported platform adapter.
  • TUI Remove and Prune reuse the unforced application planners. Prune remains repository-scoped and is unavailable until bounded planner-backed eligibility is known. Confirmation defaults to No; only a visible explicit Yes may mutate after final plan revalidation. These actions add no safety override or all-repository authority. Removing a missing/prunable registration revalidates its live repository identity and absence and never deletes replacement content or the retained branch.
  • Mouse input is additive: overlays own it, wheel input targets the hovered scrollable region, and a second click activates an already selected modal row. Every feature remains keyboard-operable and mouse capture is released on every exit path.
  • Diagnostics use a stable contextual footer and bounded typed overlay. Activity, row state, warnings, and explicit operation status remain distinct; repeated conditions deduplicate, obsolete generations cannot replace current state, and fatal exits restore the terminal. Mission retains the latest committed snapshot through transient daemon absence, timeout, contention, or ownership change and retries quietly; only an unusable local contract or explicit user action terminates it.
  • Mission submits one coalesced due set when capable identities become visible and at the fixed 15-second heartbeat. Unchanged focus submits nothing; leaving visibility stops future interest without cancelling accepted work. refresh_current (r) forces the focused capable worktree; refresh_visible (R) forces exactly the visible capable set; unbound refresh_all_indexed follows one complete convergence sweep. Rescan remains separate filesystem/Git work.
  • expand_current_group defaults to Right/l and collapse_current_group to Left/h; bindings, Commands, Enter, mouse activation, and navigation dispatch the same typed transitions.