/**
 * greenPrLogic — PURE decision helpers for the GreenPrAutoMerger
 * (green-pr-automerge-enforcement). No I/O, no clock of their own (time is
 * passed in), so every decision boundary is unit-testable on both sides.
 *
 * The orchestrating class (GreenPrAutoMerger.ts) wires these to real gh /
 * safe-merge / lease / latch / audit I/O.
 */
/** One PR as returned by the `gh pr list` GraphQL projection. */
export interface PrSummary {
    number: number;
    title: string;
    labels: string[];
    isDraft: boolean;
    headRefName: string;
    headRefOid: string;
    /** GraphQL mergeable enum: MERGEABLE | CONFLICTING | UNKNOWN. */
    mergeable: string;
    /** Worst-case rollup of the head's checks: SUCCESS | PENDING | FAILURE | null. */
    statusRollup: string | null;
    /**
     * GitHub-side native-auto-merge state derived from the `autoMergeRequest`
     * field of the `gh pr list` projection (true ⇔ the PR has auto-merge armed).
     * Optional/forward-compatible — absent on a legacy projection that did not
     * request the field. The CHEAP-PASS source of truth for "already armed,"
     * surviving a lease move with no local episode (mergerunner-auto-arm-handoff
     * Blocker 4: GitHub-side armed state is authoritative, not the local episode).
     */
    autoMergeArmed?: boolean;
}
export type HoldReason = 'draft' | 'hold-title' | 'hold-label' | null;
/** A PR is held (excluded) by a deliberate marker. Returns WHY, or null. */
export declare function holdReasonOf(pr: Pick<PrSummary, 'isDraft' | 'title' | 'labels'>): HoldReason;
export type CandidateSkip = 'not-agent-namespace' | 'held' | 'not-mergeable' | 'not-settled-green' | 'protected-paths' | 'protected-paths-unverifiable';
export interface CandidateVerdict {
    eligible: boolean;
    skip?: CandidateSkip;
    hold?: HoldReason;
}
/**
 * First-pass candidacy from the list projection alone (cheap fields). Protected
 * paths require a separate diff enumeration (handled by the caller and folded in
 * via `protectedPaths`), so this returns `eligible:true` only when the cheap
 * gates ALL pass; the caller still runs the protected-paths check before acting.
 */
export declare function classifyCandidate(pr: PrSummary, agentNamespace: string): CandidateVerdict;
/** A head branch `<agentName>/...` filter (a FILTER, not provenance — Decision 8). */
export declare function headInNamespace(headRefName: string, agentNamespace: string): boolean;
/**
 * Oldest-first selection over the FULL fetched set (the list query is pinned
 * oldest-first server-side; this is the deterministic tiebreak by PR number).
 */
export declare function selectOldest(candidates: PrSummary[]): PrSummary | null;
export interface HoldMemoryEntry {
    /** Consecutive ticks (same lease holder) the marker has been ABSENT. */
    absentTicks: number;
    /** ms timestamp the PR was first observed held this episode. */
    firstHeldAt: number;
    /** ms timestamp of the most recent absence observation. */
    lastAbsentAt?: number;
}
/**
 * Update hold memory for a PR and decide whether it is eligible to RESUME.
 * A PR observed held resumes only after the marker is absent for
 * `holdReleaseTicks` consecutive ticks AND `>= tickIntervalMs` elapsed
 * (tick-AND-time so a manual double-tick cannot collapse the window).
 */
export declare function debounceHoldRelease(mem: HoldMemoryEntry | undefined, currentlyHeld: boolean, nowMs: number, holdReleaseTicks: number, tickIntervalMs: number): {
    mem: HoldMemoryEntry | undefined;
    resumeEligible: boolean;
    transition: 'still-held' | 'released' | 'observing-release' | 'not-held';
};
export interface Episode {
    pr: number;
    headRefOid: string;
    attempts: number;
    rearmEpisodes: number;
    state: 'active' | 'gave-up';
    lastAttemptAt?: number;
    nextEligibleAt?: number;
    lastOutcome?: string;
    /**
     * NEW (mergerunner-auto-arm-handoff). When set, GitHub native auto-merge was
     * armed for this PR at `armedAt` (ms) against `armedHead` (the head sha we
     * passed to `--match-head-commit`). An armed episode is GitHub's — it stays
     * `state:'active'`, does NOT advance the ladder, and is reconciled at the top
     * of each acting tick until it merges/closes/disarms. Absent ⇒ not armed.
     */
    armedAt?: number;
    armedHead?: string;
    /**
     * Set true once the episode crosses `armedConfirmCeilingMs` (24h) still
     * OPEN + armed — the `armed-overdue` state (Close the Loop, Blocker 5). It
     * KEEPS reconciling; `armedAt` is never cleared by the ceiling alone.
     * `overdueSurfacedAt` drives the deduped re-raise cadence.
     */
    overdue?: boolean;
    overdueSurfacedAt?: number;
    /**
     * Head-keyed confirm-gap counter (Blocker D). Bounds the non-ladder retry of
     * `error:auto-arm-unconfirmed` / `error:auto-confirm-unreadable` so a
     * persistent confirm gap becomes VISIBLE rather than an invisible tick-loop.
     * Reset to 1 whenever the head changes. Absent ⇒ never had a confirm gap.
     */
    unconfirmedArmAttempts?: {
        head: string;
        count: number;
    };
}
export interface LadderConfig {
    maxAttempts: number;
    maxRearmEpisodes: number;
    backoffBaseMs: number;
}
/**
 * Fold a merge OUTCOME into the PR's episode. Returns the updated episode and
 * whether this counts as a breaker-feeding failure. `already-merged` /
 * `closed` are success-noops (never ladder failures). A `refused`/`error`
 * advances the attempt ladder with backoff; at `maxAttempts` the episode is
 * `gave-up` (re-armable by a NEW head sha up to `maxRearmEpisodes`).
 */
export declare function applyOutcome(ep: Episode, outcome: string, nowMs: number, cfg: LadderConfig, opts?: {
    armedHead?: string;
}): {
    ep: Episode;
    terminal: boolean;
    feedsBreaker: boolean;
};
/**
 * Re-arm a gave-up episode when a NEW head sha appears (the author pushed a
 * fix). Bounded by `maxRearmEpisodes` — beyond that, manual action is required.
 */
export declare function maybeRearm(ep: Episode, newHeadOid: string, cfg: LadderConfig): Episode | null;
/** Is the episode currently eligible to attempt (active + backoff elapsed)? */
export declare function episodeEligible(ep: Episode | undefined, nowMs: number): boolean;
export interface BreakerState {
    open: boolean;
    openedAt?: number;
    consecutiveBusySkips: number;
    consecutiveDeadlineKills: number;
    consecutiveTickFailures: number;
}
export interface BreakerConfig {
    busySkipBreakerThreshold: number;
    deadlineKillBreakerThreshold: number;
    breakerThreshold: number;
    breakerCooldownMs: number;
}
export declare function freshBreaker(): BreakerState;
/** Is the breaker open AND still within its cooldown? */
export declare function breakerBlocking(b: BreakerState, nowMs: number, cooldownMs: number): boolean;
/** Fold a breaker SIGNAL; returns the updated state (may open the breaker). */
export declare function feedBreaker(b: BreakerState, signal: 'busy-skip' | 'deadline-kill' | 'tick-failed' | 'reset', nowMs: number, cfg: BreakerConfig): BreakerState;
/** Boot-time invariant (B24): busy-skip budget must outlast a single merge. */
export declare function validateTimeoutInvariant(busySkipBreakerThreshold: number, tickIntervalMs: number, mergeTimeoutMs: number, mergeKillGraceMs: number): {
    ok: boolean;
    reason?: string;
};
//# sourceMappingURL=greenPrLogic.d.ts.map