/**
 * Watchdog runner — the CONSUMER that wires the merged pure pieces into a
 * working auto-nudge (RUSH-1415). The `agents watchdog` command drives it, so
 * the whole loop runs WITHOUT the Swift menu-bar.
 *
 * One tick:
 *
 *   getActiveSessions()                            (session/active.ts)
 *     -> classifyTerminal(...) per session         (watchdog/watchdog.ts)  — which are idle?
 *       -> readWatchdogTail(...) for the idle ones  (watchdog/read.ts)      — task + transcript tail
 *         -> the watchdog AGENT judges them ALL at once (watchdog/watchdog-agent.ts)
 *              — idle-but-unfinished -> nudge; idle-and-done / needs-human -> skip
 *           -> resolveInjectTargetForSession(...)  (terminal/resolve.ts)   — THE safety gate: addressable or an honest refusal
 *             -> injectIntoTerminal(target,text)   (terminal/inject.ts)    — deliver into the EXACT split, then CONFIRM
 *
 * There is no heuristic decider — no regex over the tail guessing done-vs-stuck.
 * The agent is the whole decider, given every idle session's task + tail in ONE
 * `agents run --mode plan` call per tick. The safety gate is absolute: a nudge is
 * delivered ONLY when the resolver returns `addressable: true`, and it is booked in
 * the cooldown ledger ONLY when delivery is CONFIRMED. On `addressable: false` the
 * reason is recorded to a state file the menu-bar can surface and the session is
 * SKIPPED — never a guessed / frontmost target.
 *
 * Persistence (all under ~/.agents/.cache/state/watchdog/, tray-readable):
 *   - nudges.json  — { [sessionId]: lastNudgeMs } — enforces the cooldown.
 *   - flags.json   — { [sessionId]: { reason, host, atMs } } — un-addressable stalls.
 *   - last-tick.json — the full outcome list from the most recent tick.
 *   - policy/<sessionId> — per-session sentinel: off | keep | handsoff.
 *   - rotate/<sessionId>.json — the in-place rotate state machine (rotate.ts).
 *   - rotate-skips.json — zero-healthy skip suppression: { [sessionId]: suppressUntilMs }.
 *
 * The pure logic (classifyTerminal) is imported and never re-implemented; the
 * runner only supplies its I/O (sessions, tails, clock, policy, the agent decider,
 * injection) — each an injectable seam so runner.test.ts drives real synthetic
 * sessions without a live terminal or a real `agents run`.
 */
import type { ActiveSession } from '../session/active.js';
import { type PresenceTransition } from '../session/presence.js';
import { type InjectRail, type InjectResult, type InjectTarget } from '../terminal/index.js';
import { type StallStatus, type WatchdogCandidate } from './watchdog.js';
import { type WatchdogAgentDecider } from './watchdog-agent.js';
import { type OpenBlock } from '../feed/feed.js';
import { type RotateGateResult, type RotatePhase, type RotateState } from './rotate.js';
/** Per-session policy sentinel. `keep` is the default (watchdog may nudge). */
export type WatchdogPolicy = 'off' | 'keep' | 'handsoff';
/** Stall / cooldown / dormant thresholds (ms). Defaults mirror read.ts. */
export interface WatchdogThresholds {
    stallMs: number;
    cooldownMs: number;
    dormantMs: number;
}
export declare const DEFAULT_THRESHOLDS: WatchdogThresholds;
/** The default nudge text — a short imperative, configurable via opts.nudgeText. */
export declare const DEFAULT_NUDGE_TEXT = "Continue.";
export interface WatchdogTickOptions {
    /** Actually inject when a nudge is decided. Default false (dry status). */
    nudge?: boolean;
    /** Nudge text delivered into the terminal. Default "Continue." */
    nudgeText?: string;
    /** Agent the watchdog decider runs as (and the built-in prompt). Default 'claude'. */
    smartAgent?: string;
    /** Threshold overrides. Missing fields fall back to DEFAULT_THRESHOLDS. */
    thresholds?: Partial<WatchdogThresholds>;
    /** Permit the coarse, focus-stealing Ghostty path. Off by default. */
    allowGhosttyFocus?: boolean;
    /** Pass dryRun through to injectIntoTerminal (tests set true — no real terminal). */
    injectDryRun?: boolean;
    /** Session list. Default getActiveSessions(). Tests pass synthetic sessions. */
    sessions?: ActiveSession[];
    /** Clock. Default Date.now(). Tests pin it. */
    nowMs?: number;
    /** Override the state directory (tests point at a tmpdir). */
    stateDir?: string;
    /** lastActivity (ms) for a session. Default = its transcript mtime. */
    lastActivityFor?: (s: ActiveSession) => number | undefined;
    /** Transcript tail lines for a session. Default readWatchdogTail(). */
    tailFor?: (s: ActiveSession) => string[];
    /** Per-session policy. Default = the on-disk sentinel. */
    policyFor?: (s: ActiveSession) => WatchdogPolicy;
    /**
     * The decider seam: given an idle candidate, decide nudge vs skip and craft the
     * message. Production leaves this unset — the tick judges every idle session in
     * ONE batched `agents run … --mode plan` call (watchdog-agent.ts). Tests inject a
     * synthetic per-candidate decider so the decision path is exercised without
     * shelling out.
     */
    smartDecider?: SmartDecider;
    /**
     * The batched agent decider used in production (all idle candidates → one call).
     * Default `makeWatchdogAgentDecider`. Tests inject one (e.g. returning an empty
     * map) to exercise the batched path and the no-verdict fallback without shelling
     * out. Ignored when `smartDecider` (the per-candidate test seam) is set.
     */
    agentDecider?: WatchdogAgentDecider;
    /** Open feed block for a session (parked-on-question detection). Default reads the feed. */
    openBlockFor?: (s: ActiveSession) => OpenBlock | null;
    /** Inject primitive. Default injectIntoTerminal — tests capture the resolved target. */
    injectFn?: (target: InjectTarget, text: string, opts: {
        dryRun?: boolean;
        enter?: boolean;
    }) => Promise<InjectResult>;
    /**
     * Publish a declared block on the owner's feed. Default publishBlock() — tests
     * inject a collector so no real feed dir is touched.
     */
    publishBlockFn?: (block: OpenBlock) => void;
    /** Override the canonical watchdog.log path (tests point at a tmp file). */
    logPath?: string;
    /**
     * In-place rotate of rate-limited sessions. Default = `watchdog.rotate` in
     * agents.yaml (on). Only acts when `nudge` is also set (a dry tick never
     * rotates).
     */
    rotate?: boolean;
    /** Bounded wait for the relaunched TUI to come live. Default 60s. */
    rotateReadinessMs?: number;
    /**
     * First-party health gate run BEFORE rotating. Default defaultRotateGate()
     * (collectHarnessCandidates + pickHarnessWeighted — the same selection
     * `agents run auto` makes). Tests inject a synthetic verdict.
     */
    rotateGate?: () => Promise<RotateGateResult>;
    /** New session id for the relaunch. Default crypto.randomUUID(). Tests pin it. */
    newSessionIdFor?: () => string;
    /**
     * Readiness probe: is the relaunched TUI live? Default: a transcript for the
     * new session id resolves under a known harness layout, OR a fresh active
     * session (started after the rotate began) exists in this tick's scan.
     */
    tuiLiveFor?: (state: RotateState, sessions: ActiveSession[]) => boolean;
    /** Delay between exit-sequence keystrokes. Default 300ms; tests set 0. */
    rotateKeyDelayMs?: number;
}
/** The smart brain seam: a stalled candidate in, a nudge decision out. */
export type SmartDecider = (session: ActiveSession, candidate: WatchdogCandidate) => Promise<NudgeDecision>;
/** What the tick decided for a single session — the row `--json` / the tray reads. */
export interface SessionOutcome {
    sessionId?: string;
    kind: string;
    host?: string;
    cwd?: string;
    project?: string | null;
    label?: string;
    name?: string;
    topic?: string;
    preview?: string;
    activity?: ActiveSession['activity'];
    status?: ActiveSession['status'];
    startedAtMs?: number;
    lastActivityMs?: number;
    origin?: ActiveSession['origin'];
    routineName?: string;
    machine?: string;
    owner?: string;
    /** classifyTerminal's verdict for this session. */
    stall: StallStatus['kind'];
    /** stalled duration (ms), when stalled. */
    stalledForMs?: number;
    policy: WatchdogPolicy;
    decision: 'nudge' | 'skip' | 'rotate';
    reason: string;
    /** The rotate machine's phase after this tick (decision === 'rotate'). */
    rotatePhase?: RotatePhase;
    /** The resolved rail, when delivered by injecting into a terminal split. */
    rail?: InjectRail;
    /** How the nudge was (or would be) delivered: inject | mailbox | resume. */
    via?: NudgeVia;
    /** True when resolveInjectTarget said addressable (only meaningful once we'd nudge). */
    addressable?: boolean;
    /** True when a nudge was actually delivered this tick (any mechanism). */
    injected?: boolean;
    /** The text that was (or would be) delivered. */
    nudgeText?: string;
}
/** Delivery mechanism the answer-router picked for a nudge. */
export type NudgeVia = 'inject' | 'mailbox' | 'resume';
export interface WatchdogTickResult {
    atMs: number;
    /** Whether this tick was allowed to inject (opts.nudge). */
    didNudge: boolean;
    outcomes: SessionOutcome[];
    /** Convenience counts for the menu-bar / status line. */
    counts: {
        total: number;
        stalled: number;
        nudged: number;
        unaddressable: number;
        skipped: number;
        /** Sessions the tick moved through the rotate machine (any phase). */
        rotating: number;
    };
    /**
     * RUSH-2007 Layer C: per-session presence reconciled from this tick's active
     * scan. `transitions` carries only the sessions whose connect/disconnect status
     * flipped this tick — an interactive drop is a reconnect-nudge candidate, a
     * headless remote a keep-alive. Surfaced for the tray/status; does not alter the
     * nudge decisions above.
     */
    presence: {
        connected: number;
        disconnected: number;
        transitions: PresenceTransition[];
    };
}
/**
 * On-disk per-session policy sentinel: `<stateDir>/policy/<sessionId>` whose
 * contents are `off` | `keep` | `handsoff`. Absent / unreadable / unknown → keep.
 */
export declare function readPolicySentinel(dir: string, sessionId: string): WatchdogPolicy;
/** Write a per-session policy sentinel (used by the CLI `agents watchdog policy`). */
export declare function writePolicySentinel(dir: string, sessionId: string, policy: WatchdogPolicy): void;
/** A decision for one idle session. `text` overrides the default nudge text. */
export interface NudgeDecision {
    nudge: boolean;
    reason: string;
    text?: string;
    /**
     * On a skip, `true` = the agent judged the session genuinely needs the human
     * (surface it — self-file reminder / owner page); `false` / absent = idle-and-
     * done, leave it alone. Gates the self-file reminder so a finished session is
     * never poked.
     */
    needsHuman?: boolean;
}
/**
 * Run ONE watchdog pass. Returns a structured outcome per live session; injects
 * only when `opts.nudge` is set AND the safety gate says addressable AND policy
 * permits. Persists the cooldown ledger, un-addressable flags, and a last-tick
 * snapshot to the tray-readable state dir.
 */
export declare function runWatchdogTick(opts?: WatchdogTickOptions): Promise<WatchdogTickResult>;
