/** * Worktree-estate sensor (mmnto-ai/totem#2580 slice-1) — the read-only scan * behind `totem doctor --estate`. * * Two arms, both local-git + local-fs only: * - **registered**: for every repo in the user-level registry, enumerate the * LINKED worktrees (`git worktree list --porcelain`) and classify each one * from evidence — dirty tree → active; clean + ancestry-merged into the * default branch → stale; clean + NOT ancestry-merged → `indeterminate`, * never stale. That last state is the squash-merge gap stated honestly: a * squash-merged branch leaves no ancestry edge, and this sensor has no * merged-facts source (the status-lane snapshot extension is a later * slice), so it declines to claim either way. * - **husk sweep**: candidate roots are swept one level for worktree-shaped * residue. Roots come in two kinds, and the kind decides what counts as * evidence. A CONTAINER root exists solely to hold worktrees * (`/.claude/worktrees`, or any `--root` the operator names), so an * untracked directory there is residue BY LOCATION. A STANDARD root (the * parent of a registry path or of a listed worktree) is an ordinary * working directory, so residue there needs the older positive evidence — * a dangling `.git` pointer, or a repo-name prefix plus a leftover * `node_modules`. Either way an unclassifiable directory is not reported * at all rather than guessed at. * * Registry membership is NOT protection from candidacy. Git's own worktree * list is what protects a path (cohort-overlay §2), and a genuine repo * checkout is already protected by the `.git`-DIRECTORY rule; a registry entry * only records that something was synced from a path once. Registry accounting * and disk residue are different axes — the same path can carry both a repo * row and a husk row. * * Sensor, never actuator: the only git verbs invoked are `worktree list`, * `status`, `merge-base --is-ancestor`, `log -1`, and `rev-parse`, and every * invocation carries `--no-optional-locks`. That flag is what makes the * read-only claim true rather than aspirational: `git status` otherwise * refreshes and WRITES the index, taking `index.lock` (git-status(1) § * BACKGROUND REFRESH), which in a cohort's shared worktrees would collide with * a seat's live `git add`. No registry entry is mutated and nothing is cached * across calls. Every probe failure lands as an `unscannable` row (or a * class-`unscannable` worktree row naming the failed step) so a degraded scan * can never read as a clean one. * * `safeExec` is injected rather than imported so the scan stays testable * without a real git tree (the author-sandbox.ts:21 idiom). */ import type { SafeExecOptions } from './sys/exec.js'; /** The compatibility contract with the status-lane consumer of `--estate --json`. */ export declare const ESTATE_SCHEMA_VERSION = 1; /** * The injected exec seam. Structurally identical to `safeExec`, so production * callers pass it directly and tests pass a spy. */ export type EstateExecFn = (command: string, args?: string[], options?: SafeExecOptions) => string; /** One entry of `git worktree list --porcelain`. */ export interface WorktreeListEntry { path: string; /** Commit at the worktree's HEAD, when git reported one. */ head?: string; /** Full ref (`refs/heads/`) — absent for a detached or bare entry. */ branch?: string; bare: boolean; detached: boolean; locked: boolean; lockedReason?: string; prunable: boolean; prunableReason?: string; } export type WorktreeClass = 'registered-active' | 'registered-stale' | 'registered-indeterminate' | 'registered-detached' | 'unscannable'; export type HuskEvidence = 'dangling-gitdir-pointer' | 'residue-shape' | 'deregistered-intact' /** Untracked directory under a CONTAINER root — the location is the evidence. */ | 'container-residue'; export interface EstateWorktreeRow { path: string; /** The registry repo whose worktree list produced this row. */ repoPath: string; /** Short branch name (`refs/heads/` stripped) for display. */ branch?: string; head?: string; class: WorktreeClass; dirty?: boolean; /** * `true`/`false` only when the ancestry probe actually ran and answered; * `'unknown'` whenever it could not (detached, dirty, no default branch). */ ancestryMerged: boolean | 'unknown'; /** Days since the worktree's last commit. Absent when `log -1` did not answer. */ ageDays?: number; locked?: boolean; prunable?: boolean; /** What the classification was read from, in the scan's own words. */ evidence: string; } export interface EstateHuskRow { path: string; sweptRoot: string; evidence: HuskEvidence; /** The registry repo (residue-shape) or home repo (deregistered-intact) this matched. */ matchedRepo?: string; /** Days since the directory's mtime — a husk has no commit to date from. */ ageDays?: number; } /** * Which AXIS a probe failure belongs to. The sweep axis is the one the * candidate partition is defined over; `registry` and `worktree` rows are * accounting for the registered arm and may legitimately share a path with a * husk row (the two-axes rule — a directory can be both a stale registry entry * and disk residue). */ export type EstateUnscannableSource = 'registry' | 'worktree' | 'sweep'; export interface EstateUnscannableRow { path: string; reason: string; source: EstateUnscannableSource; } export interface EstateRepoRow { path: string; lastSync?: string; /** Registry entry whose path no longer exists — reported, never probed. */ missing?: boolean; /** * Registry entry that exists but is NOT a git toplevel: git discovered an * ANCESTOR repo from it. Such an entry is never enumerated as a repo (its * worktree list would be the ancestor's), and nothing is derived from it. */ notGitRoot?: boolean; /** The ancestor repo git discovered — set with `notGitRoot`. */ enclosingRepo?: string; /** Short default-branch name in remote-tracking form (`origin/`), absent when underivable (never guessed). */ defaultBranch?: string; /** Count of LINKED worktrees (the main worktree is the repo itself). */ worktrees: number; } export interface EstateSummary { repos: number; reposMissing: number; /** Registry entries that exist but are not a git toplevel. */ reposNotGitRoot: number; /** Registry entries whose own probes failed (registry-source ledger rows). */ reposUnscannable: number; worktrees: number; active: number; stale: number; indeterminate: number; detached: number; unscannableWorktrees: number; huskCandidates: number; /** * EVERY `unscannable` ledger row, across all three axes — `reposUnscannable` * and `unscannableWorktrees` are subsets of this total, not addends beside * it. The sweep-axis count is the remainder. */ unscannable: number; } /** A root the derivation declined to sweep, with the reason it was declined. */ export interface EstateExcludedRoot { path: string; reason: string; } /** * A swept root and its KIND, which is what decides the evidence bar inside it: * a `container` root is a declared worktree location (location is evidence), a * `standard` root is an ordinary working directory (shape is evidence). * Disclosed so a consumer can tell which bar produced a given husk row. */ export interface EstateSweptRoot { path: string; kind: 'container' | 'standard'; } export interface EstateScanResult { schemaVersion: typeof ESTATE_SCHEMA_VERSION; derivedAt: string; /** * Every root actually swept, with its kind — disclosed so no cap, omission, * or evidence-bar difference is silent. */ sweptRoots: EstateSweptRoot[]; /** * Roots the derivation suppressed, so the narrowing is visible rather than * silent. A root that any other derivation path also produced is SWEPT and * never appears here. */ excludedRoots: EstateExcludedRoot[]; repos: EstateRepoRow[]; worktrees: EstateWorktreeRow[]; huskCandidates: EstateHuskRow[]; unscannable: EstateUnscannableRow[]; summary: EstateSummary; } /** The registry projection the scan needs — nothing else is read from it. */ export interface EstateRegistryEntry { path: string; lastSync?: string; } export interface EstateScanInputs { registry: EstateRegistryEntry[]; safeExec: EstateExecFn; /** Epoch ms, injected so `ageDays` is deterministic under test. */ now: number; /** Extra sweep roots (`--root`, repeatable), unioned with the derived ones. */ extraRoots?: string[]; /** * Extra STANDARD sweep roots — locations worktrees are known to live in but * that also hold other things (a recorded non-default `totem wt` root, e.g. * a shared tmp dir). Swept with shape-evidence husk criteria, never the * by-location `container-residue` class. */ extraStandardRoots?: string[]; } /** * Parse `git worktree list --porcelain`: one `worktree ` header per * entry, attribute lines until a blank line. Unknown attributes are ignored so * a newer git cannot break the parse. Git lists the MAIN worktree first, * followed by the linked ones — callers depend on that order. */ export declare function parseWorktreeListPorcelain(raw: string): WorktreeListEntry[]; /** * Scan the worktree estate. Pure per invocation: nothing persists, nothing is * written, and the result is the complete accounting — every enumerated * candidate is either classified, a husk candidate, or `unscannable`. */ export declare function scanEstate(inputs: EstateScanInputs): EstateScanResult; //# sourceMappingURL=estate-scan.d.ts.map