/**
 * Monitor (event-triggered watcher) configuration, validation, and CRUD.
 *
 * A monitor is a routine whose trigger is a *watched source* instead of a
 * *clock*. It watches a SOURCE, detects a CONDITION change, and fires an ACTION —
 * reusing the routines daemon, dispatch engine, device model, and notify path.
 *
 * Monitors are YAML files in ~/.agents/monitors/. This module owns the on-disk
 * shape (mirroring lib/routines.ts read/write helpers), hand-rolled validation
 * (mirroring validateJob — no zod), and the device-owner eligibility gate.
 */
import type { AgentId } from '../types.js';
/** Source types a monitor can watch. */
export type MonitorSourceType = 'command' | 'poll' | 'poll-http' | 'webhook' | 'ws' | 'file' | 'device';
/** Webhook source filters — reuse the github/linear matcher shape from triggers/webhook.ts. */
export interface MonitorWebhookSource {
    source: 'github' | 'linear';
    event: string;
    repo?: string;
    branch?: string;
    action?: string;
    teamKey?: string;
    label?: string;
}
/**
 * What a monitor watches. Exactly one source-payload field is populated, keyed by
 * `type`: `command`/`interval` for command/poll, `url`/`interval` for poll-http,
 * `wsUrl` for ws, `path` for file, `device` for device, `webhook` for webhook.
 */
export interface MonitorSource {
    type: MonitorSourceType;
    /** Shell command whose stdout is the observation (command, poll). */
    command?: string;
    /** Re-evaluation interval (poll, poll-http; optional for command/file/device). e.g. `30s`, `15m`, `8h`. */
    interval?: string;
    /** URL to GET (poll-http). */
    url?: string;
    /** WebSocket URL; each frame is an observation (ws). */
    wsUrl?: string;
    /** File or directory to watch (file). */
    path?: string;
    /** A registered fleet device whose health/reachability is the observation (device). */
    device?: string;
    /** Webhook trigger filters (webhook). */
    webhook?: MonitorWebhookSource;
}
/** How an observation becomes a fire. */
export type MonitorConditionMode = 'on-change' | 'match' | 'every';
export interface MonitorCondition {
    mode: MonitorConditionMode;
    /** Regex (required for `match` mode) — fire when the observation matches. */
    match?: string;
    /**
     * What counts as "the same event" for de-duplication. When set, the dedupe
     * signature is the first regex match of this expression against the
     * observation (so re-observing the same token is silent); when omitted, the
     * full observation is the signature.
     */
    dedupeKey?: string;
}
/** Action types a monitor can fire. */
export type MonitorActionType = 'run' | 'routine' | 'notify' | 'webhook-out';
/**
 * What a monitor does on a fire. Shares the run-shaped fields (agent, prompt,
 * mode, effort, timeout) conceptually with JobConfig (lib/routines.ts) so
 * dispatch reuses the routines path (executeJobDetached). The fired event is
 * injected into the prompt as `{event}`.
 */
export interface ActionConfig {
    type: MonitorActionType;
    /** run: which agent to spawn — a native harness id or a custom harness name (agents harness list). */
    agent?: AgentId | (string & {});
    /** run: the prompt; `{event}` is replaced with the fired event summary. */
    prompt?: string;
    /** run: execution mode (shared with routines). */
    mode?: 'plan' | 'edit' | 'auto' | 'skip' | 'full';
    /** run: reasoning effort (shared with routines). */
    effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'auto';
    /** run: kill the action if it runs longer than this (e.g. `10m`). */
    timeout?: string;
    /** routine: name of an existing routine to fire. */
    routine?: string;
    /** notify: override the owner channel (defaults to `notify.owner.channel`). */
    notifyChannel?: string;
    /** webhook-out: URL to POST the event to. */
    url?: string;
    /**
     * Shell command that must exit 0 after a `run`/`routine` action settles.
     * Asserts the stated effect actually happened (PHNX-2842) — e.g.
     * `gh pr view 1682 --json state --jq .state | grep -qx MERGED`. `{event}` is
     * replaced with the fired event summary, same as the prompt. Evaluated once
     * the dispatched run is no longer `running`; a failed check makes the fire
     * `ok: false` with effect `none` (`postcondition not met`), not a healthy
     * `completed`. Notify/webhook-out already have a synchronous ok and refuse
     * this field.
     */
    postcondition?: string;
}
/** Full monitor configuration (persisted as YAML in ~/.agents/monitors/). */
export interface MonitorConfig {
    name: string;
    enabled: boolean;
    source: MonitorSource;
    condition: MonitorCondition;
    action: ActionConfig;
    /**
     * Pin-to-one OWNER device — the single machine whose daemon evaluates the
     * source and fires. Exactly-once for v1 (no distributed lock). When set, only
     * that machine is eligible; everywhere else the monitor is inert.
     */
    device?: string;
    /**
     * Fleet allowlist (advanced) — each listed device evaluates and fires
     * independently, like routines' `devices`. Mutually exclusive with `device`.
     */
    devices?: string[];
    /**
     * Does this monitor's SOURCE poll a fleet-shared queue (a PR list, a ticket
     * tracker, the feed, a sync bucket) rather than the firing box's own state
     * (its repos, sessions, caches)?
     *
     * This is the SING-9 placement switch for an UNPINNED monitor (no `device` /
     * `devices`). A shared-input source has no per-box input, so every daemon
     * firing it independently is a multi-executor race on shared state — the exact
     * double-fire bug class. So an unpinned shared-input monitor fires only on the
     * single owner (`interactive.host`, else the sole box on a one-device fleet),
     * never on every daemon.
     *
     * Defaults differ by layer so the SAFE side is the default for each:
     * - a **system built-in** (`scope: 'system'`) is treated as shared-input
     *   unless it sets `sharedInput: false`, so a built-in shipped with no pin
     *   can never fan out across the fleet — a device-local built-in opts back
     *   into fleet-wide firing with `sharedInput: false`;
     * - a **user monitor** keeps its historical fleet-wide default and only
     *   becomes owner-restricted when it explicitly sets `sharedInput: true`.
     *
     * An explicit `device` / `devices` pin always wins and makes this moot — the
     * author has already chosen the executor(s).
     */
    sharedInput?: boolean;
    /** Execute the ACTION on this machine over SSH (placement), distinct from the owner that fires it. */
    runOn?: string;
    /**
     * Working directory for a `run` action, in the routines-portable form
     * (`~/…`, or relative to the execution target's home). Optional: a monitor
     * watches a source rather than owning a project, so `dispatchAction` defaults
     * it to the target's home (`~`) when unset.
     *
     * Without this, every `run` action was blocked at readiness with
     * `execution_context_missing` — `resolveJobExecutionContext` refuses an
     * agent job carrying neither `project` nor `cwd` (`lib/routine-context.ts`),
     * and a monitor had no field with which to supply one (RUSH-2681).
     */
    cwd?: string;
    /** Firehose guard: auto-pause the monitor if it fires more than `max` times per `per`. */
    rateLimit?: {
        max: number;
        per: string;
    };
    /** User-defined prompt variables (expanded like routines' variables). */
    variables?: Record<string, string>;
    /** Pin the agent version for `run` actions (omit to use the run strategy). */
    version?: string;
    /**
     * Which layer this monitor was read from — `user` (~/.agents/monitors/) or
     * `system` (the npm-shipped built-in mirror ~/.agents/.system/monitors/).
     * A DERIVED, runtime-only annotation stamped by `readMonitorFile`, never a
     * persisted YAML field: it tags a built-in in `list`/`view` (mirroring
     * routines' `(built-in)` label) and `writeMonitor` strips it before writing,
     * so materializing a user copy of a built-in always lands as `user`.
     */
    scope?: 'user' | 'system';
}
/**
 * A fired event. `summary` is injected into the action prompt as `{event}`;
 * `payload` carries the structured observation for `webhook-out` and inspection.
 */
export interface MonitorEvent {
    monitorName: string;
    firedAt: string;
    summary: string;
    payload: Record<string, unknown>;
}
/**
 * Parse a human interval string (e.g. `30s`, `15m`, `8h`, `1d`, `1h30m`) into
 * milliseconds. Unlike routines' parseTimeout, seconds are supported (polls tick
 * in seconds). Returns null on empty/unparseable/zero input.
 */
export declare function parseInterval(interval: string): number | null;
/**
 * True when an UNPINNED monitor must be placed on a single owner rather than
 * fired by every daemon — the SING-9 guard against a shared-queue double-fire.
 *
 * A `device` / `devices` pin is an explicit executor choice, so it is never
 * owner-overridden (returns false here). For an unpinned monitor the default is
 * layer-specific, SAFE side first: a **system built-in** is treated as
 * shared-input unless it opts out with `sharedInput: false`; a **user monitor**
 * keeps its fleet-wide default and only opts IN with `sharedInput: true`.
 */
export declare function requiresSingleOwner(config: Pick<MonitorConfig, 'device' | 'devices' | 'scope' | 'sharedInput'>): boolean;
/**
 * The single fleet box that owns unpinned shared-input monitors — PURE, so the
 * placement rule is unit-testable without a live tailnet. Priority:
 *
 * 1. the configured `interactive.host` (the box the operator sits at);
 * 2. else, on a fleet with no OTHER registered device, this box — a single-box
 *    install has no peer to race, so the built-in still fires here;
 * 3. else `undefined` — a multi-box fleet with no interactive host has no safe
 *    single owner, so an unpinned shared-input monitor fires NOWHERE (fail safe:
 *    a silent no-op beats a fleet-wide double-fire) until one is pinned.
 *
 * `deviceNames` is the registered fleet (registry keys); `self` is `machineId()`.
 */
export declare function resolveSharedInputOwner(interactiveHost: string | undefined, deviceNames: string[], self: string): string | undefined;
/** Resolve {@link resolveSharedInputOwner} from live config + the device registry. */
export declare function monitorSharedInputOwner(): string | undefined;
/**
 * True when the monitor may evaluate + fire on this machine. Owner semantics:
 * `device` (single owner, exactly-once) → only that machine; else `devices`
 * (allowlist) → any listed machine; else placement depends on shared-input: an
 * unpinned SHARED-INPUT monitor (a system built-in by default, or a user monitor
 * that set `sharedInput: true`) fires only on the resolved owner
 * ({@link monitorSharedInputOwner}), so a built-in that polls a fleet-shared
 * queue can never fan out across every daemon (SING-9); anything else is
 * unrestricted. Both sides normalize so `Yosemite-S0` and
 * `yosemite-s0.tailnet.ts.net` agree with `yosemite-s0`.
 *
 * `ownerHost` overrides the resolved owner for tests/callers that already know
 * it; omit it in production to resolve from config + the device registry.
 */
export declare function monitorRunsOnThisDevice(config: Pick<MonitorConfig, 'device' | 'devices' | 'scope' | 'sharedInput'>, ownerHost?: string): boolean;
/**
 * Validate a partial monitor config, returning a list of human-readable errors.
 * Hand-rolled like validateJob (lib/routines.ts) — no zod. Rejects: no source,
 * two sources, no action, two actions, match-mode without `match`, plus the
 * per-type field/shape checks.
 */
export declare function validateMonitor(config: Partial<MonitorConfig>): string[];
/**
 * List all monitor configs, unioning the user dir (~/.agents/monitors/) over the
 * built-in system dir (~/.agents/.system/monitors/). Higher layer wins by name
 * (first-seen), so a user monitor shadows a system built-in of the same name.
 */
export declare function listMonitors(): MonitorConfig[];
/**
 * Read a single monitor config by name, checking the user dir then the system
 * dir (a user monitor shadows a system built-in). Returns null if not found or
 * corrupt.
 */
export declare function readMonitor(name: string): MonitorConfig | null;
/**
 * Get the filesystem path of a monitor's YAML config in the USER dir, or null.
 * User-layer only by design — like routines' getJobPath (lib/routines.ts), its
 * caller (`agents monitors edit`) writes to the returned path, and the system
 * mirror is pull-only. To edit a system built-in, `edit` materializes a user
 * copy (prefilled via readMonitor()) rather than opening the mirror.
 */
export declare function getMonitorPath(name: string): string | null;
/** Write a monitor config to disk atomically, omitting fields that match defaults. */
export declare function writeMonitor(config: MonitorConfig): void;
/** Delete a monitor config file by name. Returns true if the file existed. */
export declare function deleteMonitor(name: string): boolean;
/** Enable or disable a monitor by name. */
export declare function setMonitorEnabled(name: string, enabled: boolean): void;
/** Check whether a monitor with the given name exists on disk. */
export declare function monitorExists(name: string): boolean;
