@agentproto/worktree 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -731,6 +731,25 @@ interface ForgeClient {
731
731
  * (PLAN.md §1.3 step 2, verified for #273/#312/#325/#271).
732
732
  */
733
733
  ensurePullHeadFetched(prNumber: number, oid: string): Promise<void>;
734
+ /**
735
+ * Every OPEN PR/MR in the repo, in one round-trip — the bulk twin of
736
+ * `pullRequestsForBranch` for callers that must protect hundreds of refs at
737
+ * once (`branch-gc.ts`: one call per ref would be a forge request per
738
+ * branch). Optional so existing test doubles keep compiling; a client that
739
+ * doesn't implement it is treated as "open-PR detection unavailable" by
740
+ * those callers, never as "no open PRs".
741
+ */
742
+ listOpenPullRequests?(): Promise<ForgePullRequestRef[]>;
743
+ /**
744
+ * Every MERGED PR/MR in the repo, in one round-trip — the bulk twin of
745
+ * `listOpenPullRequests` for the merged-head shortcut (`branch-gc.ts`): a
746
+ * branch whose tip is EXACTLY a merged PR's head commit lost nothing (that
747
+ * commit was reviewed and merged), so it is reclaimable without an LLM
748
+ * review. Optional so existing test doubles keep compiling; a client that
749
+ * doesn't implement it is treated as "merged-PR detection unavailable" by
750
+ * those callers, never as "no merged PRs".
751
+ */
752
+ listMergedPullRequests?(): Promise<ForgePullRequestRef[]>;
734
753
  }
735
754
  /** A client that always reports itself unreachable — the "no gh, no token" case. */
736
755
  declare class UnreachableForgeClient implements ForgeClient {
@@ -739,6 +758,8 @@ declare class UnreachableForgeClient implements ForgeClient {
739
758
  pullRequestsForBranch(): Promise<ForgePullRequestRef[]>;
740
759
  pullRequestsForCommit(): Promise<ForgePullRequestRef[]>;
741
760
  ensurePullHeadFetched(): Promise<void>;
761
+ listOpenPullRequests(): Promise<ForgePullRequestRef[]>;
762
+ listMergedPullRequests(): Promise<ForgePullRequestRef[]>;
742
763
  }
743
764
  /** Runs `gh` in `repoRoot` so it auto-detects owner/repo from the `origin` remote there. */
744
765
  declare class GhCliForgeClient implements ForgeClient {
@@ -749,9 +770,17 @@ declare class GhCliForgeClient implements ForgeClient {
749
770
  private parseOutput;
750
771
  pullRequestsForBranch(branch: string): Promise<ForgePullRequestRef[]>;
751
772
  pullRequestsForCommit(sha: string): Promise<ForgePullRequestRef[]>;
773
+ listOpenPullRequests(): Promise<ForgePullRequestRef[]>;
774
+ listMergedPullRequests(): Promise<ForgePullRequestRef[]>;
752
775
  ensurePullHeadFetched(prNumber: number, oid: string): Promise<void>;
753
776
  }
754
- /** Parse `owner/repo` out of a `git remote get-url origin` value (ssh or https form). */
777
+ /**
778
+ * Parse `owner/repo` out of a `git remote get-url origin` value (ssh or https
779
+ * form). The ssh form also accepts the multi-account `~/.ssh/config` alias
780
+ * convention, `git@github.com-<alias>:owner/repo` (a `Host github.com-work`
781
+ * entry that picks a key but still points at github.com), which is what a
782
+ * machine juggling two GitHub identities has as its origin.
783
+ */
755
784
  declare function parseGithubOwnerRepo(remoteUrl: string): {
756
785
  owner: string;
757
786
  repo: string;
@@ -768,6 +797,8 @@ declare class RestForgeClient implements ForgeClient {
768
797
  private pullRequestList;
769
798
  pullRequestsForBranch(branch: string): Promise<ForgePullRequestRef[]>;
770
799
  pullRequestsForCommit(sha: string): Promise<ForgePullRequestRef[]>;
800
+ listOpenPullRequests(): Promise<ForgePullRequestRef[]>;
801
+ listMergedPullRequests(): Promise<ForgePullRequestRef[]>;
771
802
  ensurePullHeadFetched(prNumber: number, oid: string): Promise<void>;
772
803
  }
773
804
  /**
@@ -799,6 +830,13 @@ declare function createForgeClient(repoRoot: string): Promise<ForgeClient>;
799
830
 
800
831
  type TreeState = {
801
832
  state: "clean";
833
+ /**
834
+ * Set only when the tree is clean BECAUSE every dirty path was on the
835
+ * caller's noise allowlist (`ComputeTreeStateOptions.noisePaths`) — the
836
+ * paths a removal must restore/delete first, since git's own
837
+ * non-`--force` `worktree remove` still sees them as dirt.
838
+ */
839
+ noise?: string[];
802
840
  } | {
803
841
  state: "dirty";
804
842
  modified: number;
@@ -812,6 +850,15 @@ type TreeState = {
812
850
  */
813
851
  newestMtimeMs: number | null;
814
852
  };
853
+ interface ComputeTreeStateOptions {
854
+ /**
855
+ * Worktree-relative paths whose dirt is known noise (e.g. a tool's lockfile
856
+ * churn that shows up in every agent worktree). A tree whose ONLY dirt is
857
+ * on this list reads `clean` (with `noise` set). Default none — callers
858
+ * opt in; see `DEFAULT_GC_NOISE_PATHS` in `gc.ts` for gc's default.
859
+ */
860
+ noisePaths?: readonly string[];
861
+ }
815
862
  /**
816
863
  * `git status --porcelain=v2` in the worktree itself, via `-C worktreePath`
817
864
  * (tree state is per-worktree, not `repoRoot`'s). The spawn's own `cwd`,
@@ -826,7 +873,7 @@ type TreeState = {
826
873
  * `worktree remove` tolerance for them (PLAN.md §0.5) — the two notions of
827
874
  * "clean" agree by construction.
828
875
  */
829
- declare function computeTreeState(repoRoot: string, worktreePath: string): Promise<TreeState>;
876
+ declare function computeTreeState(repoRoot: string, worktreePath: string, options?: ComputeTreeStateOptions): Promise<TreeState>;
830
877
  declare const integrationStateSchema: z.ZodUnion<readonly [z.ZodObject<{
831
878
  state: z.ZodLiteral<"fresh">;
832
879
  checkedAt: z.ZodString;
@@ -1056,10 +1103,29 @@ interface Classification {
1056
1103
  * `hold`.
1057
1104
  */
1058
1105
  declare function classify(tree: TreeState, integration: IntegrationState, liveness: LivenessState, nowMs?: number): Classification;
1106
+ /** How far a worktree's tip has moved from the base it is compared against. */
1107
+ interface BaseDivergence {
1108
+ /** The ref the tip is compared against (`defaultBranchRef`, e.g. `origin/main`). */
1109
+ ref: string;
1110
+ /** Commits on the tip that the base doesn't have. */
1111
+ ahead: number;
1112
+ /** Commits on the base that the tip doesn't have. */
1113
+ behind: number;
1114
+ }
1115
+ /**
1116
+ * `git rev-list --left-right --count <base>...<head>`: one read, both sides
1117
+ * of the symmetric difference. `null` for a detached/unborn tip, or when git
1118
+ * can't resolve either side (base ref never fetched, object gone) — a
1119
+ * best-effort display fact, never a reason to fail a status sweep. Spawn
1120
+ * `cwd` is `repoRoot`, like every other read here (see `computeTreeState`).
1121
+ */
1122
+ declare function computeBaseDivergence(repoRoot: string, head: string, baseRef: string): Promise<BaseDivergence | null>;
1059
1123
  interface WorktreeStatusEntry {
1060
1124
  path: string;
1061
1125
  branch: string | null;
1062
1126
  head: string;
1127
+ /** Ahead/behind vs `defaultBranchRef` — see `computeBaseDivergence`. */
1128
+ base: BaseDivergence | null;
1063
1129
  tree: TreeState;
1064
1130
  integration: IntegrationState;
1065
1131
  liveness: LivenessState;
@@ -1086,6 +1152,8 @@ interface ComputeWorktreeStatusInput {
1086
1152
  * and therefore always-"recent", delta). Defaults to `Date.now()`.
1087
1153
  */
1088
1154
  nowMs?: number;
1155
+ /** See `ComputeTreeStateOptions.noisePaths`. Default none. */
1156
+ noisePaths?: readonly string[];
1089
1157
  }
1090
1158
  /** Computes all three axes + provenance + classification for one worktree. */
1091
1159
  declare function computeWorktreeStatus(input: ComputeWorktreeStatusInput): Promise<WorktreeStatusEntry>;
@@ -1097,6 +1165,12 @@ interface ListWorktreeStatusesInput {
1097
1165
  defaultBranchRef?: string;
1098
1166
  sessionsPath?: string;
1099
1167
  now?: () => string;
1168
+ /**
1169
+ * Compute only the worktrees at these paths (compared resolved) — a
1170
+ * single-worktree read (the per-session `worktree_status` lookup) costs one
1171
+ * forge round-trip instead of one per worktree in the repo. Omitted = all.
1172
+ */
1173
+ paths?: readonly string[];
1100
1174
  }
1101
1175
  /** Enumerates every linked worktree of `repoRoot` (`git worktree list`) and computes its status. */
1102
1176
  declare function listWorktreeStatuses(input: ListWorktreeStatusesInput): Promise<WorktreeStatusEntry[]>;
@@ -1231,7 +1305,17 @@ declare function classifyForGc(tree: TreeState, integration: IntegrationState, l
1231
1305
  * only irreversible thing history nearly did to it (deleting the branch)
1232
1306
  * must not happen either. See "prunable reclaim" below.
1233
1307
  */
1234
- type GcReclaimReason = "dep-bump" | "orphan" | "prunable";
1308
+ type GcReclaimReason = "dep-bump" | "orphan" | "prunable" | InBaseReclaimReason;
1309
+ /**
1310
+ * `branch gc`'s proof tiers (`classifyTip`, branch-gc.ts) that promote a
1311
+ * clean, idle worktree out of `hold` when the forge can't: its branch's
1312
+ * CONTENT is provably in base even though no merged PR contains its tip (a
1313
+ * squash that went through another PR, a cherry-pick, a later reorg). See
1314
+ * "in-base promotion" below.
1315
+ */
1316
+ type InBaseReclaimReason = "squash-merged" | "patch-merged" | "content-merged";
1317
+ /** gc's default noise allowlist — overridable per call via `noisePaths` (`[]` disables it). */
1318
+ declare const DEFAULT_GC_NOISE_PATHS: readonly string[];
1235
1319
  /** The hold reasons this module can attach, distinct from `classify`'s snapshot-based holds. */
1236
1320
  type GcHoldReason = "live-session-cwd";
1237
1321
  /**
@@ -1250,6 +1334,11 @@ type GcHoldReason = "live-session-cwd";
1250
1334
  declare function isMechanicalDepBumpRange(repoRoot: string, baseRef: string, tipSha: string): Promise<boolean>;
1251
1335
  interface ResolveGcClassOptions extends ClassifyForGcOptions {
1252
1336
  repoRoot: string;
1337
+ /**
1338
+ * Run `branch gc`'s content ladder for a clean, idle worktree still in
1339
+ * `hold` (see "in-base promotion"). Default true.
1340
+ */
1341
+ inBaseCheck?: boolean;
1253
1342
  /** The tip commit of the worktree being classified — `worktree.head`. */
1254
1343
  tipSha: string;
1255
1344
  /** Default `"origin/main"` — matches `reconcileIntegration`'s own default. */
@@ -1336,6 +1425,8 @@ interface PlanGcInput {
1336
1425
  * protection beyond `classify`'s own snapshot-based liveness axis.
1337
1426
  */
1338
1427
  protectedPaths?: string[];
1428
+ /** Noise allowlist (see "noise allowlist" above). Default `DEFAULT_GC_NOISE_PATHS`; `[]` disables it. */
1429
+ noisePaths?: readonly string[];
1339
1430
  }
1340
1431
  /**
1341
1432
  * The dry-run plan (PLAN.md §5.2 layer 1): classify every linked worktree,
@@ -1360,6 +1451,8 @@ interface ApplyGcOptions {
1360
1451
  salvageRoot?: string;
1361
1452
  /** See `PlanGcInput.protectedPaths` — re-checked here at apply time (layer 2), independent of what the plan entry says, so a stale plan can never remove a now-protected path. */
1362
1453
  protectedPaths?: string[];
1454
+ /** See `PlanGcInput.noisePaths`. */
1455
+ noisePaths?: readonly string[];
1363
1456
  }
1364
1457
  type GcApplyOutcome = {
1365
1458
  path: string;
@@ -1429,4 +1522,471 @@ declare function reclaimOneWorktree(worktreePath: string, options: ApplyGcOption
1429
1522
  includeDetached?: boolean;
1430
1523
  }): Promise<GcApplyOutcome | null>;
1431
1524
 
1432
- export { type AgentprotoConfig, type ApplyGcOptions, CONFIG_FILENAME, type Classification, type ClassifyForGcOptions, type ComputeProvenanceOptions, type ComputeWorktreeStatusInput, ConfigError, DEFAULT_PROXY_PORT, type DaemonAgentSessionHost, type DaemonClient, ENV_VARS, type ExecOptions, type ExecResult, FileVerdictMemoStore, type ForgeClient, type ForgePullRequestRef, ForgeUnavailableError, type GateState, type GcApplyOutcome, type GcClass, type GcHoldReason, type GcPlanEntry, type GcReclaimReason, GhCliForgeClient, type GitWorktreeRef, HookError, type HookRun, type HostnameParts, InMemoryVerdictMemoStore, type IntegrationState, type ListWorktreeStatusesInput, type LivenessState, type NamedScript, type PeerService, type PlanGcInput, type ProvenanceConfidence, type ProvenanceInfo, ProxyTable, type ReconcileIntegrationInput, type ResolveGcClassOptions, type ResolveSupervisorInput, type ResolvedGcClass, type ResolvedWorktreeRuntime, RestForgeClient, type RunningProxy, SALVAGE_ROOT, SESSIONS_BUCKETS_ROOT, SESSIONS_FILE_PATH, type SalvageManifest, type SalvageResult, type SalvageWorktreeInput, type ScriptConfig, type ServiceRuntime, type ServiceStatus, ServiceSupervisor, type SessionRef, type SupervisorOptions, type TreeState, UnreachableForgeClient, VERDICT_MEMO_PATH, type VerdictMemoRecord, type VerdictMemoStore, WORKTREES_TURBO_CACHE_DIR_ENV, type WorktreeAgentInput, type WorktreeClass, type WorktreeEnvContext, type WorktreeMarker, WorktreeNotRemovableError, type WorktreeStatusEntry, allocatePort, applyGc, classify, classifyForGc, computeLiveness, computeProvenance, computeTreeState, computeWorktreeStatus, connectDaemonAgentSessionHost, createForgeClient, createProxyServer, detectDefaultBranch, disposeSupervisor, ephemeralPort, execArgv, execGit, execShell, expandGlob, getScript, getSupervisor, globToRegExp, hookEnv, isMechanicalDepBumpRange, isPortFree, listGitWorktrees, listScripts, listServices, listWorktreeStatuses, loadConfigFromBase, makeDaemonAgentSessionHost, normalizeHook, parseConfig, parseGithubOwnerRepo, peerEnv, peerEnvKey, planGc, readSessionsRegistry, readWorktreeMarker, reclaimOneWorktree, reconcileIntegration, repoLabel, resolveGcClass, resolveSupervisor, resolveWorktreesTurboCacheDir, runSetup, runTeardown, salvageWorktree, serviceEnv, serviceEnvToken, serviceHostname, serviceUrl, sessionInWorktree, sharedProxyTable, slugify, startProxy, stripPort, worktreeAgentInputSchema, worktreeAgentWorkflow, writeWorktreeMarker };
1525
+ /**
1526
+ * `branch gc`: the sibling of worktree `gc` (`gc.ts`) for refs instead of
1527
+ * worktrees — local branches (`refs/heads`), the base remote's tracking
1528
+ * branches (`refs/remotes/<remote>`), and ORPHAN tracking refs
1529
+ * (`refs/remotes/<ns>/*` whose `<ns>` is no longer a configured remote, which
1530
+ * nothing — not even `fetch --prune` — ever cleans up). Same contract as
1531
+ * worktree `gc`:
1532
+ *
1533
+ * 1. `planBranchGc` is pure read: every ref is classified `reclaim` /
1534
+ * `review` / `hold` and nothing is mutated. A dry run IS "call this,
1535
+ * print it, stop."
1536
+ * 2. `applyBranchGc` re-lists the refs and re-classifies every entry from
1537
+ * scratch immediately before touching it; a tip that moved, vanished or
1538
+ * now classifies differently is refused, never acted on.
1539
+ * 3. `hold` is never touched and `review` is never touched — only
1540
+ * `reclaim`. A reviewed-unmerged ref only becomes `reclaim` via
1541
+ * `includeReviewed` + a stored gate verdict that agreed for the SAME tip
1542
+ * sha; this module stores and reads verdicts, it never decides them.
1543
+ * 4. Every deletion is written to a restore log (sha + exact re-create
1544
+ * command) BEFORE the delete runs, so every apply is undoable.
1545
+ *
1546
+ * `reclaim` means "the work is provably in base", by a ladder of increasingly
1547
+ * expensive checks (`classifyTip`), ported from the audited
1548
+ * `branch-hygiene.mjs` maintenance script that first cleaned a 460-ref repo:
1549
+ *
1550
+ * pr-merged the tip IS the head commit (`headRefOid`) of a MERGED PR
1551
+ * — that exact commit was reviewed and merged, so nothing
1552
+ * was lost (forge shortcut in `classifyRef`: the ladder
1553
+ * still runs, but an "unmerged" verdict is overridden
1554
+ * before any reclaim/hold decision, skipping the
1555
+ * push-state/verdict lookups below it)
1556
+ * merged tip is an ancestor of base
1557
+ * squash-merged `git merge-tree --write-tree base tip` == base's tree
1558
+ * (merging it changes nothing)
1559
+ * patch-merged `git cherry` reports every commit as already applied —
1560
+ * ONLY tried when merge-tree conflicts (cherry patch-ids
1561
+ * every base commit since the merge-base: seconds per
1562
+ * branch on a busy base, and a clean merge with a
1563
+ * non-empty residual is unmerged regardless)
1564
+ * content-merged every path the branch changed is, BY BLOB, somewhere in
1565
+ * base's tree (any path — survives moves/reorgs) or
1566
+ * gitignored there, with zero `evolved` (same basename in
1567
+ * base, other content), zero `unique`, zero `deletes`
1568
+ *
1569
+ * Anything else is `unmerged` and — once older than `minAgeDays` and not
1570
+ * protected — becomes `review`, carrying everything a reviewer needs (coverage
1571
+ * summary, the files NOT provably in base, push state).
1572
+ */
1573
+
1574
+ declare const BRANCH_GC_SCOPES: readonly ["local", "remote", "orphan"];
1575
+ /** `local` = refs/heads, `remote` = the base remote's tracking refs, `orphan` = refs/remotes/<ns>/* of a remote that no longer exists. */
1576
+ type BranchRefKind = (typeof BRANCH_GC_SCOPES)[number];
1577
+ /** The verdict for one tip, in order of increasing cost (`pr-merged` is the forge shortcut that overrides an "unmerged" ladder verdict, not a check that skips the ladder). */
1578
+ type BranchStatus = "pr-merged" | "merged" | "squash-merged" | "patch-merged" | "content-merged" | "unmerged";
1579
+ /**
1580
+ * `current`: the tip shares history with base. `pre-rewrite`: it doesn't, but
1581
+ * base was re-rooted and an anchor (the old-history twin of base's root)
1582
+ * answers for it. `unrelated`: no shared history and no anchor — unmerged by
1583
+ * definition, since nothing can prove it landed.
1584
+ */
1585
+ type BranchHistory = "current" | "pre-rewrite" | "unrelated";
1586
+ type BranchGcClass = "reclaim" | "review" | "hold";
1587
+ /** Why a `reclaim` entry is reclaimable: one of the ladder's proven tiers, or a stored gate verdict (`includeReviewed`). */
1588
+ type BranchGcReclaimReason = Exclude<BranchStatus, "unmerged"> | "reviewed";
1589
+ /**
1590
+ * Why an entry is `hold`:
1591
+ * - `protected`: base or a well-known protected name (main, master, …)
1592
+ * - `worktree`: checked out in a worktree — the local branch AND its remote
1593
+ * twin (a live worktree may still push to it)
1594
+ * - `open-pr`: the head of an open PR
1595
+ * - `pr-check-unavailable`: open-PR detection could not run, so every
1596
+ * local/remote ref is held rather than silently losing that protection
1597
+ * - `young`: unmerged and younger than `minAgeDays`
1598
+ */
1599
+ type BranchGcHoldReason = "protected" | "worktree" | "open-pr" | "pr-check-unavailable" | "young";
1600
+ /**
1601
+ * Where else an unmerged tip lives — matters because deleting the only copy
1602
+ * lets `git gc` destroy the commits. Local refs: `same-tip-on-remote` /
1603
+ * `contained-in-remote` / `diverged-from-remote` / `local-only`. Orphan refs:
1604
+ * `contained-elsewhere` / `only-copy`.
1605
+ */
1606
+ type BranchPushState = "same-tip-on-remote" | "contained-in-remote" | "diverged-from-remote" | "local-only" | "contained-elsewhere" | "only-copy";
1607
+ /**
1608
+ * Content coverage of everything the branch changed (merge-base..tip), file
1609
+ * by file, against base's CONTENT rather than its history:
1610
+ * covered the exact blob exists somewhere in base (moved or copied)
1611
+ * ignored gitignored in the repo (generated output)
1612
+ * evolved a same-named file exists in base with other content
1613
+ * unique no trace in base
1614
+ * deletes the branch deletes a path base still has (an unlanded intent)
1615
+ * evolved + unique + deletes == 0 → nothing but history is lost by deleting.
1616
+ */
1617
+ interface BranchCoverage {
1618
+ residual: number;
1619
+ covered: number;
1620
+ ignored: number;
1621
+ evolved: number;
1622
+ unique: number;
1623
+ deletes: number;
1624
+ }
1625
+ interface TipClassification {
1626
+ status: BranchStatus;
1627
+ history: BranchHistory;
1628
+ /** Commits on the tip not in the compare base; `null` for an unrelated tip. */
1629
+ ahead: number | null;
1630
+ /** Commits in the compare base not on the tip; `null` when not computed. */
1631
+ behind: number | null;
1632
+ /** Set once merge-tree ran: `true` when merging into base conflicts. */
1633
+ conflicts?: boolean;
1634
+ /** Base, or the anchor for a pre-rewrite tip — what ancestry/cherry ran against. Set for `unmerged`. */
1635
+ compareBase?: string;
1636
+ mergeBase?: string;
1637
+ /** Tree of base-with-the-branch-merged, `null` when the merge conflicts. Set for `unmerged`. */
1638
+ mergedTree?: string | null;
1639
+ coverage?: BranchCoverage;
1640
+ /** The files a reviewer must look at: unique, then evolved, then deletes. Capped at 200. */
1641
+ residualFiles?: string[];
1642
+ residualFileCount?: number;
1643
+ }
1644
+ interface BranchRef {
1645
+ kind: BranchRefKind;
1646
+ /** Short name: `feat/x` for local/remote, `<ns>/feat/x` for an orphan. */
1647
+ name: string;
1648
+ /** Full ref, e.g. `refs/remotes/origin/feat/x`. */
1649
+ ref: string;
1650
+ sha: string;
1651
+ /** Committer date of the tip, ISO-8601. */
1652
+ date: string;
1653
+ author: string;
1654
+ subject: string;
1655
+ /** The remote a `remote`-kind ref belongs to. */
1656
+ remote?: string;
1657
+ }
1658
+ /** Summary of a stored verdict for this exact tip, when one exists. */
1659
+ interface BranchVerdictSummary {
1660
+ triage: BranchTriageVerdict;
1661
+ agree: boolean | null;
1662
+ reviewer: string;
1663
+ }
1664
+ interface BranchGcPlanEntry extends BranchRef, TipClassification {
1665
+ ageDays: number;
1666
+ class: BranchGcClass;
1667
+ reclaimReason?: BranchGcReclaimReason;
1668
+ holdReason?: BranchGcHoldReason;
1669
+ /** Human detail for a hold: the worktree path, `PR #n`, or why the PR check failed. */
1670
+ holdDetail?: string;
1671
+ /** Set when the status is `pr-merged`: the number of the merged PR whose head commit IS this tip. */
1672
+ mergedPr?: number;
1673
+ /** Set for unmerged local and orphan refs. */
1674
+ pushed?: BranchPushState;
1675
+ verdict?: BranchVerdictSummary;
1676
+ }
1677
+ interface BranchGcPlan {
1678
+ repoRoot: string;
1679
+ repoName: string;
1680
+ base: string;
1681
+ baseSha: string;
1682
+ baseTree: string;
1683
+ /** The remote whose tracking refs are `remote`-kind — the base's own remote. `null` when base isn't a remote ref and there is no `origin`. */
1684
+ remote: string | null;
1685
+ /** Old-history twin of base's root for a re-rooted base, else `null`. */
1686
+ anchor: string | null;
1687
+ prCheck: {
1688
+ available: boolean;
1689
+ reason?: string;
1690
+ };
1691
+ scopes: BranchRefKind[];
1692
+ minAgeDays: number;
1693
+ includeReviewed: boolean;
1694
+ generatedAt: string;
1695
+ /** Refs of OTHER configured remotes: never classified, never touched. */
1696
+ otherRemoteRefs: number;
1697
+ entries: BranchGcPlanEntry[];
1698
+ }
1699
+ /** Base content index, built once per base: path→blob, every blob, every basename. */
1700
+ interface TreeIndex {
1701
+ byPath: Map<string, string>;
1702
+ blobs: Set<string>;
1703
+ names: Set<string>;
1704
+ }
1705
+ interface LadderContext {
1706
+ repoRoot: string;
1707
+ baseSha: string;
1708
+ baseTree: string;
1709
+ anchor: string | null;
1710
+ /** Built lazily: only content coverage needs it. */
1711
+ index: () => Promise<TreeIndex>;
1712
+ /** Tip sha → "shares history with `baseSha`", when already known (the
1713
+ * plan's anchor detection asks the same question of every tip) — saves
1714
+ * the ladder's first `merge-base` per tip. */
1715
+ related?: ReadonlyMap<string, boolean>;
1716
+ }
1717
+ declare function createLadderContext(repoRoot: string, baseRef: string, anchor: string | null, related?: ReadonlyMap<string, boolean>): Promise<LadderContext>;
1718
+ /**
1719
+ * Run the ladder for one tip against `ctx`'s base. Pure read. A tip with no
1720
+ * history in common with base is checked against the anchor when there is one
1721
+ * (ancestry/cherry against the anchor; merge-tree with
1722
+ * `--merge-base=merge-base(anchor, tip)`), else it is `unmerged`/`unrelated`.
1723
+ */
1724
+ declare function classifyTip(ctx: LadderContext, tip: string): Promise<TipClassification>;
1725
+ /**
1726
+ * One-shot ladder for a caller that has a single tip and no plan (worktree
1727
+ * `gc`'s content-merged promotion). No anchor: a pre-rewrite tip reads
1728
+ * `unrelated`/`unmerged` here, which only ever errs toward keeping.
1729
+ */
1730
+ declare function classifyTipAgainstBase(repoRoot: string, baseRef: string, tip: string): Promise<TipClassification>;
1731
+ /**
1732
+ * A re-rooted base (history rewritten into a fresh snapshot commit) shares no
1733
+ * ancestor with branches cut before the rewrite. The ANCHOR is the
1734
+ * old-history commit whose tree the snapshot was taken from: searched in the
1735
+ * unrelated tips' history within 3 days before the root's date, closest tree
1736
+ * to the root wins, accepted only when fewer than 200 files apart.
1737
+ */
1738
+ declare function detectAnchor(repoRoot: string, baseSha: string, tips: readonly string[],
1739
+ /** Filled with every tip's answer (`true` = shares history with base) for
1740
+ * the ladder to reuse — see {@link LadderContext.related}. */
1741
+ related?: Map<string, boolean>): Promise<string | null>;
1742
+ declare const DEFAULT_BRANCH_GC_MIN_AGE_DAYS = 3;
1743
+ interface PlanBranchGcInput {
1744
+ repoRoot: string;
1745
+ repoName: string;
1746
+ /** Default `origin/main`. */
1747
+ base?: string;
1748
+ /** Subset of kinds to classify. Default all three. */
1749
+ scopes?: readonly BranchRefKind[];
1750
+ /** Unmerged refs younger than this are `hold`. Default 3. */
1751
+ minAgeDays?: number;
1752
+ /** Promote a `review` ref whose stored verdict agreed for the same tip sha to `reclaim` (`reviewed`). Default false. */
1753
+ includeReviewed?: boolean;
1754
+ /** Explicit anchor for a re-rooted base. Omitted → auto-detected. */
1755
+ anchor?: string;
1756
+ /** Open-PR detection (`listOpenPullRequests`). Omitted or unable → every local/remote ref is `hold` (`pr-check-unavailable`). */
1757
+ forge?: ForgeClient;
1758
+ verdicts?: BranchVerdictStore;
1759
+ /** Clock for `ageDays`. Default `Date.now()`. */
1760
+ nowMs?: number;
1761
+ /** Parallel ladder runs. Default 8. */
1762
+ concurrency?: number;
1763
+ }
1764
+ /** The dry-run plan: classify every ref in `scopes`, mutate nothing. */
1765
+ declare function planBranchGc(input: PlanBranchGcInput): Promise<BranchGcPlan>;
1766
+ interface BranchGcSummary {
1767
+ /** kind → class → count. */
1768
+ byClass: Record<BranchRefKind, Record<BranchGcClass, number>>;
1769
+ /**
1770
+ * kind → (status | "protected") → count, where "protected" = held for
1771
+ * base/protected, worktree, or open PR. Same buckets as the reference
1772
+ * script's audit summary, so the two can be compared line for line.
1773
+ */
1774
+ byStatus: Record<BranchRefKind, Record<string, number>>;
1775
+ }
1776
+ declare function summarizeBranchGcPlan(plan: BranchGcPlan): BranchGcSummary;
1777
+ interface BranchReviewCandidate {
1778
+ name: string;
1779
+ kind: BranchRefKind;
1780
+ sha: string;
1781
+ ageDays: number;
1782
+ author: string;
1783
+ subject: string;
1784
+ history: BranchHistory;
1785
+ /** Base sha. */
1786
+ base: string;
1787
+ compareBase?: string;
1788
+ mergeBase?: string;
1789
+ mergedTree?: string | null;
1790
+ conflicts?: boolean;
1791
+ ahead: number | null;
1792
+ behind: number | null;
1793
+ pushed?: BranchPushState;
1794
+ coverage?: BranchCoverage;
1795
+ residualFiles?: string[];
1796
+ residualFileCount?: number;
1797
+ /** Every ref sharing this tip (a local branch and its remote twin share one review). */
1798
+ refs: string[];
1799
+ verdict?: BranchVerdictSummary;
1800
+ }
1801
+ interface BranchReviewQueue {
1802
+ base: string;
1803
+ baseSha: string;
1804
+ anchor: string | null;
1805
+ candidates: BranchReviewCandidate[];
1806
+ }
1807
+ /**
1808
+ * The `review` entries of a plan, one per unique tip sha, with everything a
1809
+ * reviewer needs. Tips that already carry a stored verdict are skipped unless
1810
+ * `all` — verdicts are keyed by sha, so only tips that moved get re-reviewed.
1811
+ */
1812
+ declare function branchReviewQueue(plan: BranchGcPlan, options?: {
1813
+ all?: boolean;
1814
+ }): BranchReviewQueue;
1815
+ type BranchGcApplyOutcome = {
1816
+ kind: BranchRefKind;
1817
+ name: string;
1818
+ sha: string;
1819
+ result: "deleted";
1820
+ reclaimReason: BranchGcReclaimReason;
1821
+ } | {
1822
+ kind: BranchRefKind;
1823
+ name: string;
1824
+ sha: string;
1825
+ result: "held";
1826
+ holdReason?: BranchGcHoldReason;
1827
+ }
1828
+ /** `review` class: never touched by apply — the review/approval path decides these. */
1829
+ | {
1830
+ kind: BranchRefKind;
1831
+ name: string;
1832
+ sha: string;
1833
+ result: "skipped-review";
1834
+ }
1835
+ /** The ref now points somewhere else than the plan saw — never delete what wasn't classified. */
1836
+ | {
1837
+ kind: BranchRefKind;
1838
+ name: string;
1839
+ sha: string;
1840
+ result: "aborted-moved";
1841
+ currentSha: string;
1842
+ }
1843
+ /** The ref no longer exists. */
1844
+ | {
1845
+ kind: BranchRefKind;
1846
+ name: string;
1847
+ sha: string;
1848
+ result: "aborted-vanished";
1849
+ }
1850
+ /** Re-classified from scratch and no longer `reclaim`. */
1851
+ | {
1852
+ kind: BranchRefKind;
1853
+ name: string;
1854
+ sha: string;
1855
+ result: "aborted-reclassified";
1856
+ from: BranchGcClass;
1857
+ to: BranchGcClass;
1858
+ holdReason?: BranchGcHoldReason;
1859
+ } | {
1860
+ kind: BranchRefKind;
1861
+ name: string;
1862
+ sha: string;
1863
+ result: "failed";
1864
+ message: string;
1865
+ };
1866
+ interface ApplyBranchGcOptions {
1867
+ /** REQUIRED and non-empty: which kinds apply may delete. Entries of other kinds are left out of the outcomes. */
1868
+ scopes: readonly BranchRefKind[];
1869
+ forge?: ForgeClient;
1870
+ verdicts?: BranchVerdictStore;
1871
+ /** Where restore logs go. Default `~/.agentproto/branch-gc`. */
1872
+ stateDir?: string;
1873
+ nowMs?: number;
1874
+ /** Remote refs per `git push --delete`. Default 50. */
1875
+ remoteBatchSize?: number;
1876
+ }
1877
+ interface BranchGcApplyResult {
1878
+ outcomes: BranchGcApplyOutcome[];
1879
+ /** Restore log for this run, `null` when nothing was deleted. */
1880
+ restoreLog: string | null;
1881
+ }
1882
+ declare const BRANCH_GC_STATE_DIR: () => string;
1883
+ interface BranchGcRestoreEntry {
1884
+ kind: BranchRefKind;
1885
+ name: string;
1886
+ ref: string;
1887
+ sha: string;
1888
+ remote?: string;
1889
+ /** git arguments (no leading `git`) that re-create the ref, run with `-C repoRoot`. */
1890
+ argv: string[];
1891
+ /** The same, as a copy-pasteable shell command. */
1892
+ command: string;
1893
+ /** Set once the delete ran. */
1894
+ deleted?: boolean;
1895
+ }
1896
+ interface BranchGcRestoreLog {
1897
+ schema: "branch-gc-restore/v1";
1898
+ repoRoot: string;
1899
+ base: string;
1900
+ baseSha: string;
1901
+ createdAt: string;
1902
+ entries: BranchGcRestoreEntry[];
1903
+ }
1904
+ /**
1905
+ * Execute a plan. Only `reclaim` entries whose kind is in `options.scopes`
1906
+ * are touched, and each is first re-derived from scratch: the ref is
1907
+ * re-listed (moved → `aborted-moved`, gone → `aborted-vanished`) and fully
1908
+ * re-classified against fresh worktree / open-PR / verdict state (anything but
1909
+ * `reclaim` → `aborted-reclassified`). The restore log is written before the
1910
+ * first delete runs.
1911
+ */
1912
+ declare function applyBranchGc(plan: BranchGcPlan, options: ApplyBranchGcOptions): Promise<BranchGcApplyResult>;
1913
+ /** Read a restore log written by `applyBranchGc`. */
1914
+ declare function readBranchGcRestoreLog(path: string): Promise<BranchGcRestoreLog>;
1915
+ declare const BRANCH_TRIAGE_VERDICTS: readonly ["obsolete", "superseded", "salvage", "in-progress", "unclear"];
1916
+ type BranchTriageVerdict = (typeof BRANCH_TRIAGE_VERDICTS)[number];
1917
+ /**
1918
+ * One reviewer verdict for one branch tip. `gate` is the second opinion that
1919
+ * actually licenses deletion (`agree: true`); a triage-only verdict (no
1920
+ * `gate`) records the review but never makes a ref reclaimable. An agreeing
1921
+ * gate must cite evidence.
1922
+ */
1923
+ declare const branchVerdictSchema: z.ZodObject<{
1924
+ name: z.ZodString;
1925
+ sha: z.ZodString;
1926
+ triage: z.ZodObject<{
1927
+ verdict: z.ZodEnum<{
1928
+ salvage: "salvage";
1929
+ obsolete: "obsolete";
1930
+ superseded: "superseded";
1931
+ "in-progress": "in-progress";
1932
+ unclear: "unclear";
1933
+ }>;
1934
+ confidence: z.ZodNumber;
1935
+ reason: z.ZodString;
1936
+ salvage: z.ZodOptional<z.ZodString>;
1937
+ }, z.core.$strip>;
1938
+ gate: z.ZodOptional<z.ZodObject<{
1939
+ agree: z.ZodBoolean;
1940
+ verdict: z.ZodEnum<{
1941
+ salvage: "salvage";
1942
+ obsolete: "obsolete";
1943
+ superseded: "superseded";
1944
+ "in-progress": "in-progress";
1945
+ unclear: "unclear";
1946
+ }>;
1947
+ reason: z.ZodString;
1948
+ evidence: z.ZodArray<z.ZodString>;
1949
+ }, z.core.$strip>>;
1950
+ reviewer: z.ZodString;
1951
+ }, z.core.$strip>;
1952
+ type BranchVerdict = z.infer<typeof branchVerdictSchema>;
1953
+ interface BranchVerdictRecord extends BranchVerdict {
1954
+ repo: string;
1955
+ recordedAt: string;
1956
+ }
1957
+ interface BranchVerdictStore {
1958
+ /** The verdict for this exact tip, or `null`. A ref whose tip moved simply finds nothing. */
1959
+ get(repo: string, sha: string): Promise<BranchVerdictRecord | null>;
1960
+ set(record: BranchVerdictRecord): Promise<void>;
1961
+ }
1962
+ declare const BRANCH_VERDICTS_PATH: () => string;
1963
+ /** `~/.agentproto/branch-gc-verdicts.json`, keyed by (repo, tip sha). */
1964
+ declare class FileBranchVerdictStore implements BranchVerdictStore {
1965
+ private readonly path;
1966
+ private cache;
1967
+ constructor(path?: string);
1968
+ private load;
1969
+ get(repo: string, sha: string): Promise<BranchVerdictRecord | null>;
1970
+ set(record: BranchVerdictRecord): Promise<void>;
1971
+ }
1972
+ declare class InMemoryBranchVerdictStore implements BranchVerdictStore {
1973
+ private readonly map;
1974
+ get(repo: string, sha: string): Promise<BranchVerdictRecord | null>;
1975
+ set(record: BranchVerdictRecord): Promise<void>;
1976
+ }
1977
+ declare class BranchVerdictError extends Error {
1978
+ constructor(message: string);
1979
+ }
1980
+ /**
1981
+ * Validate and store one verdict. Rejects a malformed verdict, an agreeing
1982
+ * gate with no evidence, and a sha that isn't a commit in this repo.
1983
+ */
1984
+ declare function recordBranchVerdict(input: {
1985
+ repoRoot: string;
1986
+ repoName: string;
1987
+ verdict: unknown;
1988
+ store: BranchVerdictStore;
1989
+ now?: () => string;
1990
+ }): Promise<BranchVerdictRecord>;
1991
+
1992
+ export { type AgentprotoConfig, type ApplyBranchGcOptions, type ApplyGcOptions, BRANCH_GC_SCOPES, BRANCH_GC_STATE_DIR, BRANCH_TRIAGE_VERDICTS, BRANCH_VERDICTS_PATH, type BaseDivergence, type BranchCoverage, type BranchGcApplyOutcome, type BranchGcApplyResult, type BranchGcClass, type BranchGcHoldReason, type BranchGcPlan, type BranchGcPlanEntry, type BranchGcReclaimReason, type BranchGcRestoreEntry, type BranchGcRestoreLog, type BranchGcSummary, type BranchHistory, type BranchPushState, type BranchRef, type BranchRefKind, type BranchReviewCandidate, type BranchReviewQueue, type BranchStatus, type BranchTriageVerdict, type BranchVerdict, BranchVerdictError, type BranchVerdictRecord, type BranchVerdictStore, type BranchVerdictSummary, CONFIG_FILENAME, type Classification, type ClassifyForGcOptions, type ComputeProvenanceOptions, type ComputeTreeStateOptions, type ComputeWorktreeStatusInput, ConfigError, DEFAULT_BRANCH_GC_MIN_AGE_DAYS, DEFAULT_GC_NOISE_PATHS, DEFAULT_PROXY_PORT, type DaemonAgentSessionHost, type DaemonClient, ENV_VARS, type ExecOptions, type ExecResult, FileBranchVerdictStore, FileVerdictMemoStore, type ForgeClient, type ForgePullRequestRef, ForgeUnavailableError, type GateState, type GcApplyOutcome, type GcClass, type GcHoldReason, type GcPlanEntry, type GcReclaimReason, GhCliForgeClient, type GitWorktreeRef, HookError, type HookRun, type HostnameParts, type InBaseReclaimReason, InMemoryBranchVerdictStore, InMemoryVerdictMemoStore, type IntegrationState, type LadderContext, type ListWorktreeStatusesInput, type LivenessState, type NamedScript, type PeerService, type PlanBranchGcInput, type PlanGcInput, type ProvenanceConfidence, type ProvenanceInfo, ProxyTable, type ReconcileIntegrationInput, type ResolveGcClassOptions, type ResolveSupervisorInput, type ResolvedGcClass, type ResolvedWorktreeRuntime, RestForgeClient, type RunningProxy, SALVAGE_ROOT, SESSIONS_BUCKETS_ROOT, SESSIONS_FILE_PATH, type SalvageManifest, type SalvageResult, type SalvageWorktreeInput, type ScriptConfig, type ServiceRuntime, type ServiceStatus, ServiceSupervisor, type SessionRef, type SupervisorOptions, type TipClassification, type TreeIndex, type TreeState, UnreachableForgeClient, VERDICT_MEMO_PATH, type VerdictMemoRecord, type VerdictMemoStore, WORKTREES_TURBO_CACHE_DIR_ENV, type WorktreeAgentInput, type WorktreeClass, type WorktreeEnvContext, type WorktreeMarker, WorktreeNotRemovableError, type WorktreeStatusEntry, allocatePort, applyBranchGc, applyGc, branchReviewQueue, branchVerdictSchema, classify, classifyForGc, classifyTip, classifyTipAgainstBase, computeBaseDivergence, computeLiveness, computeProvenance, computeTreeState, computeWorktreeStatus, connectDaemonAgentSessionHost, createForgeClient, createLadderContext, createProxyServer, detectAnchor, detectDefaultBranch, disposeSupervisor, ephemeralPort, execArgv, execGit, execShell, expandGlob, getScript, getSupervisor, globToRegExp, hookEnv, isMechanicalDepBumpRange, isPortFree, listGitWorktrees, listScripts, listServices, listWorktreeStatuses, loadConfigFromBase, makeDaemonAgentSessionHost, normalizeHook, parseConfig, parseGithubOwnerRepo, peerEnv, peerEnvKey, planBranchGc, planGc, readBranchGcRestoreLog, readSessionsRegistry, readWorktreeMarker, reclaimOneWorktree, reconcileIntegration, recordBranchVerdict, repoLabel, resolveGcClass, resolveSupervisor, resolveWorktreesTurboCacheDir, runSetup, runTeardown, salvageWorktree, serviceEnv, serviceEnvToken, serviceHostname, serviceUrl, sessionInWorktree, sharedProxyTable, slugify, startProxy, stripPort, summarizeBranchGcPlan, worktreeAgentInputSchema, worktreeAgentWorkflow, writeWorktreeMarker };