/** * Worktree-orphan scan + prune primitives for `cleo doctor`. * * Background: the T9550/T9580 SSoT bug in `getCleoDirAbsolute()` (fixed in * v2026.5.83) caused stray `.cleo/` directories to be created underneath * `/.claude/worktrees//[/]`. Those orphans were * never cleaned up — `cleo doctor --audit-worktree-orphans` (read-only) * lists them with full provenance, and `cleo doctor --prune-worktree-orphans` * archives them to a tarball, appends a JSONL audit-log line per prune, * then removes them. * * Also provides `auditWorktreeOrphansComprehensive` (T9808) — a broader * scan that reads the live `git worktree list` and checks: * 1. Orphan `.cleo/` dirs inside ANY registered git worktree path. * 2. Worktrees outside the canonical XDG location * (`/worktrees///`). * 3. Rogue `.cleo/worktrees/` DIRECTORY (council D009 — only a `.json` * sentinel file is permitted there; a full directory is a sign that the * old in-tree worktrees convention leaked into the project `.cleo/`). * * Security model: * - Scan limits depth to 3 (`worktrees//[/]/.cleo/`) — see * `MAX_SCAN_DEPTH` below. * - Prune validates every `orphanPath` is under the resolved * `/.claude/worktrees/` root via `path.resolve` + * prefix check before any `rm -rf`. Symlinks are resolved with * `realpathSync` so attackers can't escape via a planted link. * - Tarball is written FIRST (atomic safety net). Audit line is appended * before removal so a crash mid-rm still records the intent. * - All paths logged in `.cleo/audit/worktree-prune.jsonl`. * * @task T9790 * @task T9808 * @epic T9790 * @epic T9808 */ import type { ComprehensiveAuditResult, OrphanEntry, OrphanScanResult, PruneResult } from '@cleocode/contracts'; /** * Options for {@link scanWorktreeOrphans} and * {@link scanWorktreeOrphansBudgeted}. */ export interface ScanOptions { /** * Override the scan root. Defaults to `/.claude/worktrees/`. * Mainly for tests. */ worktreesRoot?: string; /** * Per-level fan-out hard-stop. When the number of entries in a single * directory level reaches this limit the scan aborts and returns with * `isPartial: true, partialReason: 'overflow'`. * * Default: 500. Set to `Infinity` to disable. * Only honoured by {@link scanWorktreeOrphansBudgeted}. */ maxEntriesPerLevel?: number; /** * Per-level soft-warn threshold. When entries in a level exceed this value * a warning message is added to the result but the scan continues (up to * `maxEntriesPerLevel`). * * Default: 100. Set to `Infinity` to disable. * Only honoured by {@link scanWorktreeOrphansBudgeted}. */ softWarnEntriesPerLevel?: number; /** * Maximum wall-clock milliseconds for the entire scan. When exceeded the * scan aborts and returns with `isPartial: true, partialReason: 'timeout'`. * * Default: `undefined` (no timeout). Typically set to `30_000` (30 s) by * the CLI. * Only honoured by {@link scanWorktreeOrphansBudgeted}. */ timeoutMs?: number; } /** * Options for {@link pruneWorktreeOrphans}. */ export interface PruneOptions { /** * Directory under which the `.tar.gz` archive is written. Created if * missing. Recommended: * `/.cleo/backups/`. */ archiveDir: string; /** * Absolute path to the audit log file. Each pruned entry produces one * JSONL line. Recommended: * `/.cleo/audit/worktree-prune.jsonl`. */ auditLogPath: string; /** * When `true`, no tarball is written, no audit line is appended, and no * `rm` is invoked. The returned `PruneResult.pruned` lists what would * have happened. */ dryRun?: boolean; /** * The project root the orphans must be contained within. The prune step * REJECTS any entry whose `orphanPath` does not resolve to a descendant * of `/.claude/worktrees/`. Defaults to the parent of * `archiveDir`'s grandparent (`/../..`) when not provided. */ projectRoot: string; } /** * Recursively search for `.cleo/` directories under `worktreesRoot`, up * to {@link MAX_SCAN_DEPTH} levels deep, and return one {@link OrphanEntry} * per match. * * Walks every sub-directory but stops descending once it has either (a) * found a `.cleo/` directory at the current level (it does NOT descend * INTO the orphan further) or (b) exceeded `MAX_SCAN_DEPTH`. This bounds * the cost on huge worktrees. * * @param projectRoot - The project root that owns `.claude/worktrees/`. * @param opts - See {@link ScanOptions}. * @returns Discovered orphans, sorted by `orphanPath` ascending for stable output. * * @example * const orphans = await scanWorktreeOrphans('/mnt/projects/cleocode'); * for (const o of orphans) console.log(o.orphanPath, o.sizeBytes); */ export declare function scanWorktreeOrphans(projectRoot: string, opts?: ScanOptions): Promise; /** * Width-budgeted wrapper around {@link scanWorktreeOrphans}. * * Adds three safety valves (T9962): * - **Hard stop** at `maxEntriesPerLevel` entries per directory level * (default 500). Aborts immediately and returns * `isPartial: true, partialReason: 'overflow'`. * - **Soft warn** at `softWarnEntriesPerLevel` (default 100). Scan * continues but `softWarnMessage` is set in the result. * - **Timeout** at `timeoutMs` milliseconds. Aborts and returns * `isPartial: true, partialReason: 'timeout'`. * * Existing callers of `scanWorktreeOrphans` keep their original behaviour * unchanged. New code (e.g. the CLI) should call this function instead. * * NOTE: This code is scheduled for replacement by the Rust worktrunk-core * rewrite in T9977/T9986. Do NOT over-engineer. * * @param projectRoot - The project root that owns `.claude/worktrees/`. * @param opts - See {@link ScanOptions}. * @returns {@link OrphanScanResult} wrapping the discovered orphans. * * @task T9962 */ export declare function scanWorktreeOrphansBudgeted(projectRoot: string, opts?: ScanOptions): Promise; /** * Atomically archive + remove the given orphan entries. * * Workflow per call: * 1. Validate every entry's `orphanPath` is under * `/.claude/worktrees/`. Entries that fail are * pushed into `rejected[]` and skipped. * 2. Write a single `.tar.gz` containing all surviving orphans to * `/worktree-orphans-.tar.gz`. * 3. Append one JSONL line per pruned entry to `auditLogPath`. * 4. `rm -rf` each orphan. * * In dry-run mode the function performs step 1 only and returns the * proposed prune plan in `pruned[]` (with `archivePath: null`). * * @param entries - The orphans to prune. Typically the output of * {@link scanWorktreeOrphans}. * @param opts - See {@link PruneOptions}. * @returns A {@link PruneResult} describing what happened. * * @example * const orphans = await scanWorktreeOrphans(root); * const result = await pruneWorktreeOrphans(orphans, { * projectRoot: root, * archiveDir: join(root, '.cleo/backups'), * auditLogPath: join(root, '.cleo/audit/worktree-prune.jsonl'), * }); * console.log(`archived to ${result.archivePath}, freed ${result.totalSizeBytes} bytes`); */ export declare function pruneWorktreeOrphans(entries: OrphanEntry[], opts: PruneOptions): Promise; /** * Comprehensive worktree anomaly audit (T9808 / council D009). * * Reads `git worktree list` for the given project root and produces a * structured report of three anomaly classes: * * 1. **`orphan-cleo-dir`** — any `.cleo/` directory found inside a git * worktree path (these should never exist; worktrees are read-only * consumers of the main project's `.cleo/`). * * 2. **`non-canonical-location`** — a worktree exists at a path that is * NOT under the canonical XDG root * (`/worktrees//`). The main repo checkout is * the only accepted non-canonical entry. * * 3. **`rogue-worktrees-directory`** — `/.cleo/worktrees/` * exists as a **directory**. Council D009 mandates that only a single * `.json` sentinel file may live at that path; a directory means the * old in-tree worktree convention leaked into the project `.cleo/`. * * Returns a {@link ComprehensiveAuditResult} with all anomalies sorted by * `kind` then `path` for stable output. `count > 0` means anomalies were * found; exit code 2 is recommended on non-zero count. * * @param projectRoot - Absolute path to the project root (contains `.git/`). * @returns Audit result. * * @example * const result = await auditWorktreeOrphansComprehensive('/mnt/projects/cleocode'); * if (result.count > 0) process.exitCode = 2; * * @task T9808 */ export declare function auditWorktreeOrphansComprehensive(projectRoot: string): Promise; //# sourceMappingURL=worktree-orphans.d.ts.map