/**
 * The coarse bucket a held worktree falls into. These are the three the ticket
 * names, and they demand very different follow-ups:
 *
 * - `unmerged-commits` — the one that matters. The branch carries commits with
 *   no patch-equivalent upstream: real work visible to nobody. Candidate for
 *   "push the branch or open a PR", never deletion.
 * - `uncommitted-changes` — a dirty tree. Could be live work, could be build
 *   output nobody will miss. Needs a human eye, not an automatic action.
 * - `undeterminable` — a broken or locked checkout whose merge state or status
 *   could not be read. Fails closed by design; must be re-examined, never swept.
 */
export type HeldBucket = 'unmerged-commits' | 'uncommitted-changes' | 'undeterminable';
/**
 * The specific reason inside a bucket. `undeterminable` splits into the two the
 * sweep distinguishes so an operator can tell a locked index (`status-unreadable`)
 * from a repo with no resolvable default ref (`merge-state-unknown`).
 */
export type HeldReason = 'unmerged-commits' | 'uncommitted-changes' | 'status-unreadable' | 'merge-state-unknown';
/** One classified held worktree — the structured record the sweep threw away. */
export interface HeldWorktree {
    /** Repo root that owns this worktree (the dir holding `.agents/worktrees`). */
    repo: string;
    /** basename(repo), for compact tables. */
    repoName: string;
    /** Worktree slug (the dir under `.agents/worktrees`). */
    name: string;
    /** Absolute path to the worktree. */
    path: string;
    /** Checked-out branch, or null for a detached HEAD. */
    branch: string | null;
    bucket: HeldBucket;
    reason: HeldReason;
    /**
     * Commits on this worktree with no patch-equivalent upstream (`git cherry`).
     * -1 means the answer could not be established.
     */
    unmergedCommits: number;
    /** `git status --porcelain` line count; -1 means it could not be read. */
    dirtyFiles: number;
    /** True when `<branch>` already exists on `origin` (so the work is visible). */
    hasRemoteBranch: boolean;
    /** Whole days since the worktree dir was last modified. */
    ageDays: number;
    /** Disk footprint of the worktree dir in bytes, or -1 if it could not be read. */
    sizeBytes: number;
}
/**
 * Resolve the default branch ref to compare against: `origin/HEAD` when set,
 * else whichever of `origin/main` / `origin/master` exists. Returns null when
 * none resolve, which makes every worktree `merge-state-unknown` rather than
 * silently comparing against nothing.
 */
export declare function resolveDefaultRef(repoRoot: string): Promise<string | null>;
/**
 * Count commits with no patch-equivalent upstream. Returns -1 when the answer
 * cannot be established, which the classifier treats as `merge-state-unknown`.
 *
 * Patch-id (`git cherry`), NOT ancestry, is load-bearing: this fleet
 * rebase-merges, so a landed branch's SHAs are rewritten and
 * `merge-base --is-ancestor` reports *not merged* for work that fully landed.
 * It also subsumes the unpushed check — an unpushed commit has no upstream
 * equivalent, so it surfaces as unmerged rather than needing a separate probe.
 */
export declare function countUnmergedCommits(worktreePath: string, defaultRef: string | null): Promise<number>;
/** One `git worktree list --porcelain` record. */
interface PorcelainEntry {
    path: string;
    branch: string | null;
    detached: boolean;
}
export declare function parseWorktreePorcelain(out: string): PorcelainEntry[];
/**
 * True when `child` is `parent` or sits beneath it, compared on path
 * boundaries. A raw `startsWith` makes `.../fix-1103` look like it is inside
 * `.../fix-110`, so the primary checkout would be mistaken for a neighbour.
 */
export declare function isInside(child: string, parent: string): boolean;
/** Everything the bucket decision depends on, gathered once. */
export interface HeldFacts {
    /** null for detached HEAD. */
    branch: string | null;
    /** `git status --porcelain` line count; -1 = could not read. */
    dirtyFiles: number;
    /** `git cherry` count; -1 = could not determine. */
    unmergedCommits: number;
}
/**
 * Decide which bucket a worktree falls into, or null when it is neither stranded
 * nor dirty nor broken (clean and fully upstream — nothing to surface). Pure.
 *
 * Precedence is chosen for SURFACING, not for the sweep's deletion decision:
 * the work-loss signal wins. A determinable `unmerged-commits` is reported as
 * exactly that even when the tree is also dirty, because the recoverable work
 * is the priority — an operator seeing `uncommitted-changes` would push nothing.
 * Only when the merge state itself is unreadable do we fall to `undeterminable`,
 * because then we genuinely cannot tell whether work is stranded.
 */
export declare function classifyHeld(facts: HeldFacts): {
    bucket: HeldBucket;
    reason: HeldReason;
} | null;
/**
 * Classify every worktree registered under one repo and return only the held
 * ones. Read-only — no `git worktree remove`, no `branch -d`, no push. The
 * primary checkout (the first porcelain record, always the repo root) is never
 * a `.agents/worktrees` slug, so it is skipped implicitly.
 */
export declare function collectHeldWorktrees(repoRoot: string): Promise<HeldWorktree[]>;
/**
 * Discover repo roots that own a `.agents/worktrees` container beneath
 * `searchHome`. Mirrors the sweep's discovery: match directory SHAPE, prune the
 * heavy dirs so a large home stays fast. Read-only.
 */
export declare function discoverWorktreeRepos(searchHome: string, maxDepth?: number): Promise<string[]>;
/** Discover + classify every held worktree beneath `searchHome`. Read-only. */
export declare function collectHeldWorktreesUnder(searchHome: string): Promise<HeldWorktree[]>;
/** A held set grouped by bucket, with the itemised entries kept. */
export interface HeldSummary {
    total: number;
    buckets: Record<HeldBucket, HeldWorktree[]>;
}
/** Group a flat held list into its three buckets. Pure. */
export declare function summarizeHeld(held: HeldWorktree[]): HeldSummary;
/** One device's contribution to a fleet-wide roll-up. */
export interface DeviceHeld {
    device: string;
    held: HeldWorktree[];
}
/** A fleet-wide roll-up: every device's held worktrees, still bucketed. Pure. */
export interface FleetHeldSummary extends HeldSummary {
    devices: {
        device: string;
        total: number;
    }[];
}
/**
 * Merge several devices' held sets into one fleet roll-up, stamping each entry
 * with its source device so the `unmerged-commits` bucket names where the
 * stranded work lives. Pure — the SSH fan-out that produces the input lives in
 * the command layer.
 */
export declare function aggregateHeld(perDevice: DeviceHeld[]): FleetHeldSummary;
/** Outcome of the safe recovery action on one stranded worktree. */
export interface PushResult {
    name: string;
    branch: string | null;
    pushed: boolean;
    reason: string;
}
/**
 * The safe automatic action for the `unmerged-commits` bucket (ticket step 3):
 * PUBLISH the branch so the work becomes visible, never remove anything.
 *
 * Gated hard, fails closed:
 *  - only a worktree whose live classification is still `unmerged-commits`;
 *  - only when the branch does NOT already exist on `origin` — a branch that is
 *    already pushed (including one behind an open PR) needs nothing, and this
 *    also means we never force-update a remote ref;
 *  - a slug-shaped worktree name only, never shelled otherwise.
 *
 * `git push` sets no `--force`: it fast-forwards a new ref or fails loud. A
 * failure is returned, never swallowed.
 */
export declare function pushStrandedBranch(repoRoot: string, wt: HeldWorktree): Promise<PushResult>;
export {};
