/**
 * Scheduled job (routine) configuration and run history management.
 *
 * Routines are YAML files in ~/.agents/routines/ that define recurring or
 * one-shot agent tasks. This module handles CRUD operations on job configs,
 * run metadata persistence, prompt variable expansion, and one-shot "at" time
 * scheduling.
 */
import { Cron } from 'croner';
import { type ResolvedExecutionContext, type ProjectResolution, type PlacementMode, type RoutineKind, type ContextFsProbe } from '../routine-context.js';
import type { AgentId, RunStrategy } from '../types.js';
import type { LoopConfig } from '../loop.js';
import { JobScheduler } from '../scheduler.js';
export declare function fireConditionLabel(job: JobConfig): string;
export declare function nextRunForDisplay(job: JobConfig, scheduler: JobScheduler): Date | null;
export declare function nextRunLabel(job: JobConfig, scheduler: JobScheduler, now: Date): string;
export declare function localLatestRun(job: JobConfig): RunMeta | null;
export declare function listJobsForDisplay(cwd?: string): JobConfig[];
export declare function buildRoutineListJson(): Record<string, unknown>[];
/** One routine's live scheduler-status row for `agents routines status --json`. */
export interface RoutineStatusRow {
    name: string;
    /** The single device this routine is pinned to fire on, or null when unpinned. */
    ownerDevice: string | null;
    /** True when `devices:` names more than one device — no single owner (doctor flags it). */
    ambiguousDevicePin: boolean;
    /** Every device this routine is enabled on. */
    enabledDevices: string[];
    /** Whether THIS device is the one that fires the routine. */
    runsHere: boolean;
    enabled: boolean;
    overdue: boolean;
    nextRun: string | null;
    /** Terminal status of the last local fire: completed/failed/timeout/missed/blocked/skipped/running, or null if it never ran here. */
    lastStatus: RunMeta['status'] | null;
    /** The last fire's failure reason, when it did not complete cleanly. Named to match `list --json`'s `failureReason`. */
    failureReason: string | null;
    lastRunStartedAt: string | null;
    lastRunCompletedAt: string | null;
    /** Present only while a run is genuinely in flight on THIS device (a live local child or a host-placed run) — never a provisional pre-spawn claim. */
    inFlight: {
        runId: string;
        pid: number | null;
        startedAt: string;
        triggerKind: RunMeta['triggerKind'] | null;
    } | null;
}
/**
 * The per-routine rows behind `agents routines status --json`. Distinct from
 * {@link buildRoutineListJson}: this is the scheduler-truth surface the daemon
 * owns — per routine it names the single owner device, the last fire's outcome
 * and error, and any in-flight spawn — the fields an operator (or the menu bar /
 * ext) needs to answer "did this routine fire, and is one running right now?"
 * that the definition-shaped `list --json` does not carry (PHNX-3215).
 *
 * `monitorRunningJobs()` runs first to reap runs whose process has exited, then
 * `inFlight` is gated on {@link isRunGenuinelyInFlight} — NOT on `status ===
 * 'running'` alone, because a provisional pre-spawn claim is `running` with a
 * null pid that the reaper does not touch (RUSH-2640).
 *
 * The routine set is the schedulable one ({@link listJobs}, the same
 * `getDaemonStatus`/`routines status` counts), not the display set
 * {@link buildRoutineListJson} uses — a scheduler-status surface names what the
 * daemon can actually fire, not discoverable-but-unmaterialised project routines.
 */
export declare function buildRoutineStatusRows(): RoutineStatusRow[];
/** Tool/site/directory allow-list for sandboxed job execution. */
export interface JobAllowConfig {
    tools?: string[];
    sites?: string[];
    dirs?: string[];
}
/**
 * Where a routine's job body executes when the daemon fires it.
 * Distinct from `devices` (which daemon may *fire*) and from the CLI `--device`
 * remote-management passthrough (manage routines *on* another machine).
 */
export type HostStrategy = 'local' | 'host' | 'fleet' | 'cloud';
export declare const HOST_STRATEGIES: readonly HostStrategy[];
/**
 * Provenance for a routine that was materialised from a project
 * (`.agents/routines/*.yml` synced into the user layer after opt-in).
 */
export interface JobSource {
    /** Always `project` today; reserved for future layers. */
    kind: 'project';
    /** Absolute path to the project root that owns the YAML. */
    projectPath: string;
    /** GitHub `owner/repo` when the project has a GitHub origin remote. */
    repo?: string;
    /** Branch at last sync (when known). */
    branch?: string;
    /** Short commit SHA at last sync (when known). */
    commit?: string;
}
/** GitHub webhook events a routine can be triggered by. */
export type GithubTriggerEvent = 'pull_request' | 'push' | 'issue_comment' | 'workflow_run';
/** Canonical set of accepted GitHub trigger events — single source for validation. */
export declare const GITHUB_TRIGGER_EVENTS: readonly GithubTriggerEvent[];
/** Linear webhook resource types a routine can be triggered by. */
export type LinearTriggerEvent = 'Issue' | 'IssueLabel' | 'Comment' | 'Project' | 'Cycle';
/** Canonical set of accepted Linear trigger events — single source for validation. */
export declare const LINEAR_TRIGGER_EVENTS: readonly LinearTriggerEvent[];
/**
 * Map a user-facing `--on` alias to a canonical GitHub trigger event.
 * Accepts the canonical names plus friendly shortcuts (e.g. `pr`, `pr_opened`
 * → `pull_request`, `comment` → `issue_comment`). Returns null when unknown.
 */
export declare function normalizeTriggerEvent(input: string): GithubTriggerEvent | null;
/**
 * Event-based fire condition for a routine — an alternative (or complement) to
 * `schedule`. Incoming webhooks whose source-specific filters match fire the job
 * through the same dispatch path a cron fire uses. See
 * `src/lib/triggers/webhook.ts`.
 */
export interface GithubJobTrigger {
    type: 'github_event';
    event: GithubTriggerEvent;
    /** `owner/name` — when set, only payloads for this repo match. */
    repo?: string;
    /** git branch (ref short name) — when set, only payloads for this branch match. */
    branch?: string;
    /** GitHub webhook action, e.g. `opened`, `synchronize`, or `labeled`. */
    action?: string;
    /** Required GitHub label name. For `pull_request.labeled`, this is the added label. */
    label?: string;
}
export interface LinearJobTrigger {
    type: 'linear_event';
    event: LinearTriggerEvent;
    /** Linear action, e.g. `create`, `update`, `remove`. */
    action?: string;
    /** Issue identifier prefix such as `RUSH`; useful when one webhook spans teams. */
    teamKey?: string;
    /** Required issue label name. */
    label?: string;
    /** Fire on a transition INTO this Linear state (e.g. `Plan`): the current state
     *  must match AND the delivery's `updatedFrom` must record a state change, so a
     *  later edit that leaves the issue in this state does not re-fire (RUSH-2539). */
    stateTo?: string;
    /** Previous Linear state name that must match (e.g. `Triage`). */
    stateFrom?: string;
}
export type JobTrigger = GithubJobTrigger | LinearJobTrigger;
/**
 * Full configuration for a routine (persisted as YAML).
 *
 * A job fires on a `schedule` (cron), on a `trigger` (event/webhook), or both.
 * `schedule` remains a first-class field; trigger-only jobs omit it and are
 * skipped by the cron scheduler (they fire only via the webhook receiver).
 */
export interface JobConfig {
    name: string;
    /** Cron expression. Optional when `trigger` is set (event-only routine). */
    schedule?: string;
    /** Event/webhook fire condition. Optional when `schedule` is set. */
    trigger?: JobTrigger;
    /**
     * Which agent runs the routine — a native harness id, or the name of a
     * custom harness (`agents harness list`), which the runner delegates to
     * `agents run <name>` the same way workflow jobs are. Optional — omitted
     * for `workflow`/`command` routines. Exactly one of agent/workflow/command
     * must be set.
     */
    agent?: AgentId | (string & {});
    workflow?: string;
    /**
     * A plain shell command run directly instead of an agent/workflow — no LLM,
     * no auth, no rotation, no tokens, no sandbox overlay. For deterministic
     * housekeeping routines (version-check, `npm i -g`, `git pull`, notify).
     * Mutually exclusive with `agent` and `workflow`.
     */
    command?: string;
    mode: 'plan' | 'edit' | 'auto' | 'skip' | 'full';
    effort: 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'auto';
    timeout: string;
    enabled: boolean;
    prompt: string;
    timezone?: string;
    repo?: string;
    /**
     * Singular execution anchor: the named project (`agents projects`) whose base
     * directory the routine's run lands in. Optional. Metadata-only `projects[]`
     * (below) is NEVER used for execution — this field is. Resolution happens on
     * the execution TARGET (`resolveRoutineExecutionContext`, routine-context.ts),
     * never from the daemon's own cwd: a project with a usable `defaultPath`/`root`
     * gives the base directory; a rootless Linear-imported project gives no base,
     * so a bare relative `cwd` then anchors at the target user's `$HOME`.
     *
     * CLI flag is `--project-anchor` (not `--project`, which is the repeatable
     * grouping-metadata flag that writes `projects[]`). The YAML key is the shorter
     * singular `project` because it is unambiguous there.
     */
    project?: string;
    /**
     * Portable execution directory for the routine's run. Optional. A relative
     * value resolves under the `project` base when that base is usable, otherwise
     * under the execution target's `$HOME` (so a Linear-imported rootless project
     * can still name `cwd: src/github.com/acme/app`). A `~/`-anchored value is the
     * target's home-relative path; an absolute path under the target home is
     * normalized to the portable `~/…` form on save. An absolute path outside the
     * home is only allowed for local-pinned routines — host/fleet/cloud placement
     * pauses it as non-portable. Supersedes the legacy `remoteCwd`, which the
     * one-shot migration folds into this field.
     */
    cwd?: string;
    /**
     * Fleet allowlist — restrict this routine to specific devices. When omitted
     * or empty, the routine is unrestricted and fires on every device running the
     * scheduler. When set, only devices whose `machineId()` matches any entry
     * (via `normalizeHost`) schedule, fire, catch up, or count this job as
     * overdue; everywhere else it is inert and `run` refuses with a pointer.
     */
    devices?: string[];
    /**
     * Whether a fire this device missed (daemon down, laptop asleep, wedged event
     * loop) is run late. Defaults to true: croner only schedules forward from
     * "now", so without catch-up a missed fire is simply lost and the routine
     * silently does not run.
     *
     * Set `catchup: false` for a routine whose value is tied to its clock — a
     * 9am standup brief is worthless at 3pm. An opted-out routine still records
     * the miss (a `missed` run), it just is not re-run.
     */
    catchup?: boolean;
    /**
     * When this routine came into existence, ISO 8601. Stamped once by
     * {@link writeJob}, like `actor`.
     *
     * Overdue detection needs it: `detectOverdueJobs` walks back a week for the
     * most recent expected fire, so without a floor a brand-new routine is
     * "overdue" for occurrences that happened before it was written. Harmless
     * when catch-up was a manual command; with auto-catchup it would run every
     * newly created routine once, immediately.
     */
    createdAt?: string;
    /**
     * Environment variables injected into the spawned run, on top of the sandbox
     * overlay's own. Merged by `buildSpawnEnv`, so it applies to both the
     * foreground and detached execution paths.
     */
    env?: Record<string, string>;
    /**
     * Execution placement — run the job body on this machine over SSH (a
     * registered host, device, capability tag, or user@host) instead of locally.
     * Distinct from `devices`: `devices` says which daemon may FIRE the job,
     * `host` says where the dispatched run EXECUTES. CLI flag: `--run-on`
     * (`--device` on routines commands already means "manage routines on that
     * machine" via the remote passthrough).
     *
     * When `hostStrategy` is set, it owns placement semantics; `host` is then
     * only required for `hostStrategy: host` (or when strategy is inferred from
     * a bare `host:` field for back-compat).
     */
    host?: string;
    /**
     * Where the job body should run when the daemon fires it.
     * - `local`  — on the firing machine (default / current behavior)
     * - `host`   — on the named `host` over SSH (maps to `--run-on`)
     * - `fleet`  — pick one online registered device per run (no cross-device
     *              double-fire; the firing pin stays on `devices`)
     * - `cloud`  — dispatch via the agent's native cloud provider
     *
     * CLI flag: `--placement` (not `--device`, which is the remote-management
     * passthrough). Omitted strategy falls back to `host` when `host:` is set,
     * otherwise `local`.
     */
    hostStrategy?: HostStrategy;
    /** Working directory on the host for `host:`-placed runs. */
    remoteCwd?: string;
    /**
     * Provenance for routines materialised from a project
     * (`<project>/.agents/routines/*.yml` → user-layer copy after opt-in).
     * Absent for hand-authored user/system routines.
     */
    source?: JobSource;
    variables?: Record<string, string>;
    sandbox?: boolean;
    allow?: JobAllowConfig;
    config?: Record<string, unknown>;
    version?: string;
    /**
     * Explicit per-routine version/account selection strategy — the same
     * vocabulary as `agents run --strategy` (RUN_STRATEGIES). Overrides
     * `run.<agent>.strategy` from the FIRING device's own agents.yaml
     * (getConfiguredRunStrategy) so a routine's selection policy travels with the
     * definition instead of depending on whichever box happens to fire it.
     * Conflicts with `version:` (an exact pin leaves nothing to select) —
     * validateJob rejects the pair.
     */
    strategy?: RunStrategy;
    /**
     * Pin this routine to a signed-in account by identity (its login email, or its
     * account key) instead of rotating. At launch it resolves to whichever
     * installed version currently holds that account and runs pinned — no
     * `balanced` rotation, no usage-read refresh, no failover onto other accounts.
     *
     * This is the durable way to keep concurrent unattended routines off a *shared*
     * Claude OAuth credential: the refresh token is single-use and rotates
     * server-side on every refresh, so two routines running the same account
     * concurrently (on one box or across the fleet) mutually revoke each other —
     * the `401 OAuth access token has been revoked` storm (RUSH-1957). Give each
     * routine (or each device's routines) a distinct account and no run rotates a
     * credential out from under another.
     *
     * Prefer this over `version:`, which pins a version *number* that is GC'd on
     * the next agent upgrade — when the pinned version disappears the routine
     * silently falls back to `balanced` and the stampede returns.
     */
    account?: string;
    runOnce?: boolean;
    endAt?: string;
    /**
     * When set, the job resumes this existing agent session id at fire time
     * (`agents run <agent> --resume <id>`) instead of starting a fresh conversation,
     * so the actual session reopens with full context and `prompt` becomes its next
     * turn. Powers self-scheduled wake-ups (e.g. /hibernate). claude/codex only.
     */
    resume?: string;
    /** When set, executeJob runs this job through the loop driver instead of once. */
    loop?: LoopConfig;
    /**
     * Actor id of whoever CREATED this routine (`resolveActor().id`, stamped by
     * `writeJob` at creation and preserved across edits). Propagated into each
     * fired run's env and RunMeta so an unattended cron traces back to the person
     * who scheduled it, not the `UNRESOLVED@<host>` a live resolve would give.
     * RUSH-2020.
     */
    actor?: string;
    /**
     * Named projects this routine belongs to. Metadata-only: organises the
     * routine under a project group in `agents routines list` and the menu bar;
     * has no effect on scheduling or execution.
     *
     * Special values:
     * - `["*"]` — routine applies to all defined projects (the "All projects" group).
     * - A single name — routine belongs to that specific project.
     * - Multiple names — routine spans several projects ("Cross-project" group).
     * - Absent/empty — routine belongs to no project ("Operations" group).
     */
    projects?: string[];
    /**
     * Set only by `lib/monitors/dispatch.ts` on the one-off job it synthesizes for
     * a monitor's `run` action. Such a job is NOT a routine: it has no definition
     * file, `agents routines` never lists it, and its name can therefore never
     * appear in this device's routine activation manifest
     * (`~/.agents/devices/<machine>/agents.yaml` → `routines:`). Gating it on that
     * manifest refused every monitor action fleet-wide with `wrong_owner` and an
     * empty allowlist (RUSH-2681), so {@link jobRunsOnThisDevice} skips the
     * manifest for it. Exactly-once ownership is already resolved BEFORE dispatch
     * by the monitor's own `device:` pin (`monitorRunsOnThisDevice`,
     * `lib/monitors/config.ts`) — re-gating here was double-gating on the wrong
     * key.
     *
     * Deliberately narrow: a monitor's or webhook handler's `routine` action fires a
     * REAL routine, which keeps its activation gate, so a routine defined but not
     * activated on this device is still refused.
     *
     * Runtime-only, enforced at both ends of the schema boundary: `writeJob`
     * strips it, and `readJobFileResult` refuses a definition that carries it
     * (a hand-authored one would otherwise fire on every box regardless of
     * activation, since the daemon's load path never calls `validateJob`).
     */
    dispatchedBy?: 'monitor' | 'webhook';
}
/**
 * Canonical form of a routine's `projects` field: drop non-string and empty
 * entries and deduplicate while preserving first-seen order. This is the single
 * source of truth for project-name normalization, applied at the schema
 * boundary (`writeJob` before persistence) and at grouping (`computeProjectGroupKind`)
 * so a hand-authored YAML with duplicates (`projects: [myapp, myapp]`) is
 * treated identically to the canonical single-entry form everywhere.
 *
 * Returns `undefined` when nothing survives, so callers can omit the field.
 */
export declare function normalizeProjects(projects: string[] | undefined): string[] | undefined;
/**
 * A routine's project bucket, discriminated by `kind` so buckets are never keyed
 * on their human display label. A named project called literally "Operations" or
 * "Cross-project" is `{ kind: 'named', name }` and can never collide with the
 * `operations` / `cross` special buckets that happen to share those titles.
 */
export type ProjectGroup = {
    kind: 'named';
    name: string;
} | {
    kind: 'all';
} | {
    kind: 'cross';
} | {
    kind: 'operations';
} | {
    kind: 'unknown';
};
/**
 * Classify a routine's `projects` field into a discriminated {@link ProjectGroup}.
 * Duplicates are collapsed first ({@link normalizeProjects}), so `[myapp, myapp]`
 * is a single named project, not a "Cross-project" span.
 *
 * @param projects - The routine's projects array (may be undefined).
 * @param knownProjectNames - The set of currently defined project names (from `listProjectDefs`).
 */
export declare function computeProjectGroupKind(projects: string[] | undefined, knownProjectNames: Set<string>): ProjectGroup;
/** Human display title for a {@link ProjectGroup}. */
export declare function projectGroupTitle(group: ProjectGroup): string;
/**
 * Stable bucket key for a {@link ProjectGroup}. Named projects key on their name
 * under a `named:` prefix; specials key on their `kind` under a `special:` prefix.
 * The two namespaces can never collide, so a project named "Operations" gets its
 * own bucket separate from the no-project "Operations" special.
 */
export declare function projectGroupKey(group: ProjectGroup): string;
/** Sort rank for a {@link ProjectGroup}: named projects first, then specials in a fixed order. */
export declare function projectGroupOrder(group: ProjectGroup): number;
/**
 * Compute the display group label for a routine's `projects` field.
 *
 * Kept as the label-returning form for the JSON `projectGroup` field and any
 * text consumer; grouping and ordering use the discriminated
 * {@link computeProjectGroupKind}/{@link projectGroupKey} instead so buckets are
 * never keyed on the label.
 *
 * @param projects - The routine's projects array (may be undefined).
 * @param knownProjectNames - The set of currently defined project names (from `listProjectDefs`).
 *
 * Returns one of:
 * - A specific project name — when `projects` has exactly one known name.
 * - `"All projects"` — when `projects` is `["*"]`.
 * - `"Cross-project"` — when `projects` has multiple distinct known entries.
 * - `"Operations"` — when `projects` is absent or empty.
 * - `"Unknown projects"` — when any entry is no longer a defined project (stale).
 */
export declare function computeProjectGroup(projects: string[] | undefined, knownProjectNames: Set<string>): string;
/** A real-filesystem {@link ContextFsProbe} for readiness checks on this machine. */
export declare function realFsProbe(): ContextFsProbe;
/** Classify a routine by its body kind — governs the execution-context fallback rules. */
export declare function jobRoutineKind(config: Pick<JobConfig, 'agent' | 'workflow' | 'command'>): RoutineKind;
/**
 * Resolve a routine's execution context (working directory + structural/fs
 * readiness) by bridging its `project`/`cwd` fields into the pure
 * {@link resolveRoutineExecutionContext} resolver. Local placement resolves
 * against this machine's `$HOME` with a real filesystem probe; a caller may
 * inject a different target home / probe (e.g. `null` to defer existence for a
 * remote target).
 */
export declare function resolveJobExecutionContext(config: Pick<JobConfig, 'name' | 'project' | 'cwd' | 'agent' | 'workflow' | 'command'>, opts?: {
    targetHome?: string;
    mode?: PlacementMode;
    probe?: ContextFsProbe | null;
    projectResolution?: ProjectResolution;
}): ResolvedExecutionContext;
/** Metadata for a single job execution, persisted as JSON in the run directory. */
export interface RunMeta {
    jobName: string;
    runId: string;
    agent?: AgentId | (string & {});
    /**
     * Resolved agent version this run launched under. Re-pointed to each failover
     * attempt's version as the single-shot chain advances (runner.ts), so it names
     * the version that actually ran and wrote the transcript. Recorded so
     * `archiveRoutineTranscripts` can find the transcript in the per-version home a
     * config-dir-relocating agent (claude/codex) writes to, and so account
     * attribution can name the home that ran (RUSH-2271). Unset for
     * command/self-updating runs.
     */
    version?: string;
    workflow?: string;
    /** The shell command that ran, for command-mode routines (no agent). */
    command?: string;
    pid: number | null;
    /** Process birth time (epoch ms) recorded at spawn for pid-reuse detection. */
    spawnedAt?: number;
    /** Configured execution deadline persisted for daemon-restart recovery. */
    timeoutMs?: number;
    /**
     * `missed` is not an execution outcome — it is the record that a scheduled
     * fire never happened (the daemon was down, asleep, or wedged when it came
     * due). Without it a miss leaves no trace at all and the listing keeps
     * showing the previous run's status as if it were current. Written by
     * `claimMissedFire` (catchup.ts), never by the runner.
     *
     * `blocked` and `skipped` are pre-execution terminals that leave a visible
     * record even though no agent process ran (the plan's history contract):
     * - `blocked` — a fire-time readiness rejection (bad context, dead auth,
     *   untrusted workspace). No agent process was spawned. Distinct from `failed`,
     *   which means a process started and failed.
     * - `skipped` — the attempt lost a claim (`skipReason`): a duplicate schedule
     *   slot, an already-active run it would overlap, or a wrong device owner.
     */
    status: 'running' | 'completed' | 'failed' | 'timeout' | 'missed' | 'blocked' | 'skipped';
    /**
     * How this attempt was triggered. Answers "why did this run exist" for a
     * record that may have no transcript (a blocked/skipped attempt).
     */
    triggerKind?: 'schedule' | 'catchup' | 'manual' | 'webhook' | 'event';
    /**
     * The scheduler's intended UTC fire time (ISO), for a `schedule`/`catchup`
     * attempt. The atomic single-fire claim keys on (routine, scheduledFor): a
     * duplicate cron delivery for the same slot resolves to this same run rather
     * than launching a second time.
     */
    scheduledFor?: string;
    /** Resolved execution context (routine-context.ts), recorded before preflight. */
    project?: string;
    requestedCwd?: string;
    resolvedCwd?: string;
    readiness?: {
        code: string;
        message: string;
        repair?: string;
    };
    /** Why a `skipped` attempt launched nothing. */
    skipReason?: 'duplicate_slot' | 'active_run' | 'wrong_owner';
    /** The run this attempt deferred to (the winning duplicate slot / active run). */
    activeRunId?: string;
    startedAt: string;
    completedAt: string | null;
    exitCode: number | null;
    /** Machine-readable failure reason when the run did not complete successfully. */
    errorMessage?: string;
    /** Wall-clock duration of the run in milliseconds (set when the run finishes). */
    duration?: number;
    /** Set for `host:`-placed runs — where the job body executes (no local pid). */
    host?: string;
    /** The host-task sidecar id backing a `host:` run; the daemon monitor
     *  finalizes the run by reconciling it against the remote `.exit`. */
    hostTaskId?: string;
    /** Cloud provider task id when `hostStrategy: cloud` dispatched the run. */
    cloudTaskId?: string;
    /** Cloud provider id when the run was cloud-dispatched. */
    cloudProvider?: string;
    /**
     * Actor id of the routine's CREATOR (stamped at creation, carried from the job
     * config). Answers "whose scheduled run is this" for an unattended cron fire,
     * where resolving the actor live would only yield `UNRESOLVED@<host>`. RUSH-2020.
     */
    actor?: string;
    /**
     * Actor id that TRIGGERED this particular run (`resolveActor().id` at fire
     * time): a person for a manual `agents routines run`, `UNRESOLVED@<host>` for
     * an unattended scheduled fire. Distinct from {@link actor} (the creator).
     */
    triggeredBy?: string;
}
/**
 * Finalize a run record with a terminal status, computing `duration` from
 * `startedAt` and the completion timestamp. Keeps failure-reason population
 * centralized so every completion path writes the same machine-readable fields.
 */
export declare function finalizeRunMeta(meta: RunMeta, status: RunMeta['status'], exitCode: number | null, opts?: {
    errorMessage?: string;
    completedAt?: string;
}): void;
/**
 * True when the job may execute on this machine: no `devices` allowlist (or
 * empty), or the allowlist includes this device. Both sides go through
 * `normalizeHost` so `Yosemite-S0`, `yosemite-s0.tailnet.ts.net`, and
 * `yosemite-s0` all agree. Every fire path (cron scheduler, webhook,
 * catchup/overdue, manual run) gates on this.
 *
 * A monitor- or webhook-dispatched job ({@link JobConfig.dispatchedBy} set) skips
 * the routine activation manifest: it is not a routine, so its name can never be
 * a member and the lookup could only ever answer "not activated here"
 * (RUSH-2681 for monitors; the same hole broke every `run.agent`/`run.workflow`
 * webhook handler, which the receiver logged as `fired` while the run record read
 * `skipped` with an empty allowlist). Ownership was already decided upstream — by
 * the monitor's `device:` pin, or by the one box the webhook was delivered to.
 */
export declare function jobRunsOnThisDevice(config: Pick<JobConfig, 'name' | 'devices' | 'dispatchedBy'>): boolean;
/**
 * The ONE device that owns a routine — the single daemon allowed to fire it.
 *
 * `devices` is an allowlist, and every listed device used to fire
 * independently, so a routine pinned to two boxes ran **twice** per schedule:
 * two full agent sessions doing identical work, burning double the quota. On
 * this fleet seven routines were in that state, e.g. `security-sweep` running
 * at 15:30:02 on one box and 15:30:03 on the other, both completing.
 *
 * Ownership is a pure function of the config — the first entry in normalized
 * sort order — so every daemon independently reaches the same answer with no
 * lease, no cross-device coordination, and no split brain when the fleet
 * partitions. A multi-entry pin is a misconfiguration
 * ({@link hasAmbiguousDevicePin}); this keeps such a routine running exactly
 * once instead of silently dropping it, while `validateJob` refuses to create
 * a new one and `agents doctor` surfaces the existing ones.
 *
 * Returns null when the routine is unrestricted (empty or omitted `devices`).
 */
export declare function routineOwnerDevice(config: Pick<JobConfig, 'devices'>): string | null;
/**
 * Does this routine name more than one distinct device? Such a pin used to mean
 * "fire on each of them"; it now means "fire only on the first", which is
 * almost certainly not what the author intended either way — so it is reported
 * as a misconfiguration rather than silently reinterpreted.
 */
export declare function hasAmbiguousDevicePin(config: Pick<JobConfig, 'devices'>): boolean;
/** One routine whose `devices` names more than one machine, with its resolved owner. */
export interface AmbiguousDevicePin {
    name: string;
    devices: string[];
    /** The device that now fires it — the rest are inert. */
    owner: string;
}
/**
 * Every routine carrying a multi-device pin. Surfaced by `agents doctor` and
 * `agents routines list` so an existing misconfiguration is visible rather than
 * silently reinterpreted: before ownership became singular each of these fired
 * once per listed device, doubling the work and the agent spend.
 */
export declare function findAmbiguousDevicePins(cwd?: string): AmbiguousDevicePin[];
/**
 * Resolve the effective host strategy for a job.
 * Bare `host:` without an explicit strategy implies `host` (back-compat with
 * pre-hostStrategy YAML). Otherwise default to `local`.
 */
export declare function resolveHostStrategy(config: Pick<JobConfig, 'hostStrategy' | 'host'>): HostStrategy;
/**
 * Parse a CLI `--placement` value into a HostStrategy, or null when empty.
 * Throws a human-readable Error for unknown values.
 */
export declare function parseHostStrategy(raw: string | undefined | null): HostStrategy | null;
/**
 * Strategies that dispatch the job body off the firing machine. Without a
 * `devices` pin every daemon in the fleet would fire and each would dispatch
 * once — N× duplicate runs. Callers pin to this machine when the user did not
 * set an explicit allowlist.
 */
export declare function placementRequiresFiringPin(strategy: HostStrategy): boolean;
/** Human presentation of a device-affinity mismatch for commands and runner. */
export interface JobEligibilityResult {
    /** Full human message, e.g. "Job 'NAME' can only run on: a, b". */
    message: string;
    /** One-line copy-paste suggestion, e.g. "agents routines run NAME --device a". */
    suggestion: string;
    /** Comma-separated allowed devices label, e.g. "a, b". */
    allowedLabel: string;
    /** First allowed device (normalized), useful for the suggested host. */
    firstHost: string;
}
/**
 * Return null when the job may run here; otherwise return a structured,
 * human-friendly eligibility failure. Centralizes the message/suggestion
 * construction so manual run, executeJob, and executeJobDetached stay in
 * sync. Scheduler/webhook/overdue paths continue to use jobRunsOnThisDevice.
 */
export declare function checkJobDeviceEligibility(config: Pick<JobConfig, 'name' | 'devices' | 'dispatchedBy'>): JobEligibilityResult | null;
/**
 * List all job configs, scanning project > user > system routine dirs.
 * Higher layers shadow lower ones of the same name (first-seen wins): a project
 * routine shadows a user routine, and a user routine shadows a system routine
 * (`~/.agents/.system/routines/`, shipped via gh:phnx-labs/.agents-system).
 * When a same-name project routine wins for inspection, the user-layer
 * `devices` allowlist is overlaid only if the project routine does not declare
 * its own allowlist, so CWD project discovery cannot hide an operational fleet
 * pin or erase a project-authored one.
 * Project discovery is opt-in via `cwd`; the daemon (which calls `listJobs()`
 * with no argument) sees user + system routines.
 */
export declare function listJobs(cwd?: string): JobConfig[];
/**
 * Read a single job config by name, checking project > user > system.
 * Same-name project routines keep the user-layer `devices` allowlist only when
 * the project routine does not declare its own allowlist, for the same reason
 * as listJobs().
 * Project discovery is opt-in via `cwd`; daemon callers pass no argument and
 * resolve user + system routines.
 */
export declare function readJob(name: string, cwd?: string): JobConfig | null;
/**
 * The outcome of reading one routine file: a config, or the reason it is inert.
 * Every `problem` here means the daemon will not run the routine.
 */
export type RoutineReadResult = {
    config: JobConfig;
    problem: null;
} | {
    config: null;
    problem: string;
};
/**
 * Read one routine file, preserving WHY it failed.
 *
 * `readJobFile` collapses all four fail-closed paths to `null`, which is right
 * for the loaders (an inert routine must not run) but leaves a broken routine
 * invisible in every view — the one routine an operator most needs to see. Same
 * reading, reason kept, for diagnostic surfaces like `agents inspect --routines`.
 */
export declare function readJobFileResult(filePath: string): RoutineReadResult;
/** Write a job config to disk, omitting fields that match defaults.
 *
 * Updates the one existing supported extension (.yml or .yaml) atomically.
 * New routines are written as .yml. If both extensions exist for the same
 * name, the write fails explicitly so we never choose or drop a sibling.
 */
export declare function writeJob(config: JobConfig): void;
/**
 * Serialize a job config, preserving the on-disk formatting of an existing file.
 *
 * A full `yaml.stringify(config)` re-emits the whole document — restyling every
 * scalar (unquoting `schedule`, re-wrapping the folded `prompt` block, reordering
 * keys). When a routine is only being toggled (pause/resume) or re-pinned
 * (`devices --set`), that rewrites the entire file, leaving the git-backed
 * `~/.agents` tree perpetually dirty so `agents repo pull` refuses to sync across
 * the fleet. To keep the diff to the field that actually changed, we edit the
 * existing document in place and only re-render touched nodes; untouched nodes
 * (notably the large `prompt` block) keep their byte-for-byte formatting.
 *
 * `existingText` is the current file contents, or null for a new file. New,
 * unparseable, and non-mapping documents fall back to canonical `yaml.stringify`.
 */
export declare function serializeJob(output: Record<string, unknown>, existingText: string | null): string;
/** Delete a job config file by name. Returns true if the file existed. */
export declare function deleteJob(name: string): boolean;
/** Enable or disable a job by name. */
export declare function setJobEnabled(name: string, enabled: boolean): void;
/** Materialize legacy definition state into this device's activation manifest. */
export declare function migrateLegacyRoutineActivation(): boolean;
/** Validate a partial job config, returning a list of human-readable errors. */
export declare function validateJob(config: Partial<JobConfig>): string[];
/** Validate a job trigger block, returning a list of human-readable errors. */
export declare function validateTrigger(trigger: unknown): string[];
/** True when a job's endAt has already elapsed. False when endAt is unset or in the future. */
export declare function isPastEndAt(config: Pick<JobConfig, 'endAt'>, now?: Date): boolean;
export interface OneShotScheduleParts {
    minute: number;
    hour: number;
    day: number;
    month: number;
}
export declare function parseOneShotLikeSchedule(schedule: string | undefined | null): OneShotScheduleParts | null;
export declare function isOneShotLikeSchedule(schedule: string | undefined | null): boolean;
export declare function isOneShotRoutine(config: Pick<JobConfig, 'schedule' | 'runOnce'>): boolean;
export declare function oneShotScheduleFireDate(schedule: string | undefined | null, now?: Date, timezone?: string): Date | null;
export declare function isPastOneShotRoutine(config: Pick<JobConfig, 'schedule' | 'runOnce' | 'timezone'>, now?: Date): boolean;
export declare function hasCompletedOneShotRun(config: Pick<JobConfig, 'name' | 'schedule' | 'runOnce' | 'timezone'>, now?: Date): boolean;
export declare function shouldPurgeCompletedOneShotRoutine(config: Pick<JobConfig, 'name' | 'schedule' | 'runOnce' | 'timezone'>, now?: Date): boolean;
/**
 * Context passed to `resolveJobPrompt` when a job is fired by a webhook. Lets
 * prompts use `{{issue.identifier}}`, `{{updatedFrom.state.name}}`, etc.
 */
export interface WebhookContext {
    source: string;
    event: string;
    action?: string;
    issue?: unknown;
    updatedFrom?: unknown;
    pull_request?: unknown;
    repository?: unknown;
}
/**
 * Substitute `{{dotted.path}}` placeholders in a string using a webhook context.
 * Missing values are replaced with an empty string.
 */
export declare function substituteWebhookPrompt(prompt: string, context: WebhookContext): string;
/**
 * Substitute `{{dotted.path}}` placeholders in a string destined for a SHELL,
 * quoting every substituted value so payload content cannot break out of it.
 *
 * `run.command` is executed through a shell, and its context is built from an
 * external webhook payload — `issue.title`, `issue.description`, and the GitHub
 * `pull_request` fields are free text any outside contributor can set. Pasting
 * those in raw (as {@link substituteWebhookPrompt} does, correctly, for prompts)
 * turns an operator's `echo {{issue.title}}` into a command-injection sink.
 *
 * The template itself is operator-authored and stays unquoted, so pipes,
 * redirects, and `&&` in the configured command keep working. Only the
 * interpolated values are quoted.
 *
 * POSIX `sh` quoting: wrap in single quotes and close/escape/reopen for any
 * embedded single quote. `exec` uses `cmd.exe` on Windows, which does not
 * honour these rules — see `assertShellSubstitutionSupported`.
 */
export declare function substituteWebhookCommand(command: string, context: WebhookContext): string;
/**
 * Refuse a `run.command` carrying placeholders on a platform whose shell we
 * cannot safely quote for. `child_process.exec` runs through `cmd.exe` on
 * Windows, where POSIX single-quoting is not a quoting mechanism at all, so
 * {@link substituteWebhookCommand} would not contain a hostile value.
 *
 * Fail loud rather than execute something we cannot prove is safe.
 */
export declare function assertShellSubstitutionSupported(command: string, platform?: NodeJS.Platform): void;
/** Expand built-in and user-defined template variables in a job's prompt string. */
export declare function resolveJobPrompt(config: JobConfig, context?: WebhookContext): string;
/** Parse a human-readable timeout string (e.g. "10m", "2h", "1h30m", "3d", "1w") into milliseconds.
 *  Accepts combinations of w (weeks), d (days), h (hours), m (minutes).
 *  Returns null if the string is empty, matches nothing, totals zero, or exceeds 1 week.
 */
export declare function parseTimeout(timeout: string): number | null;
/** List all run metadata entries for a job, sorted chronologically. */
export declare function listRuns(jobName: string): RunMeta[];
/** Get the most recent run for a job, or null if never run. */
export declare function getLatestRun(jobName: string): RunMeta | null;
/**
 * Get the most recent COMPLETED run for a job, or null if none has completed.
 * Used to resolve `{last_report}` so a failed run's error text (e.g. an auth
 * login prompt written into report.md) can never be injected into the next
 * run's prompt.
 */
export declare function getLatestCompletedRun(jobName: string): RunMeta | null;
/** Duration + outcome rollup for a job's run history. */
export interface RoutineStats {
    /** Total run records (any status, including `missed`). */
    count: number;
    failed: number;
    missed: number;
    avgMs: number;
    p50: number;
    p95: number;
}
/**
 * Fold a job's run history (`listRuns`) into a duration + outcome summary.
 * `missed` fires (no process ever ran) carry no `duration` and are excluded
 * from the latency percentiles but still counted in `count`/`missed`.
 */
export declare function routineStats(jobName: string): RoutineStats;
/** Persist run metadata to its run directory as meta.json. */
export declare function writeRunMeta(meta: RunMeta): void;
/** Read run metadata from disk. Returns null if missing or corrupt. */
export declare function readRunMeta(jobName: string, runId: string): RunMeta | null;
/**
 * Runs directory for a single job, with the (untrusted) job name contained to a
 * single segment beneath the runs dir — same guard as `getJobHomePath`. The name
 * comes from routine YAML and can arrive via a synced config repo; every runs-dir
 * sink (run dir, meta read/write, last-report read) routes through here so a
 * crafted `name` like `../../../../tmp/x` can't `mkdirSync`/write `stdout.log`,
 * `meta.json`, or `report.md` outside `~/.agents/.history/runs`.
 */
export declare function getJobRunsDir(jobName: string): string;
/** Get the filesystem path for a specific run's directory. */
export declare function getRunDir(jobName: string, runId: string): string;
/**
 * The run id a scheduled fire is recorded under — derived from its intended UTC
 * fire time so the SAME slot always maps to the SAME run directory. This is what
 * makes the single-fire claim meaningful: a duplicate cron delivery for one slot
 * computes the same id and loses the atomic `mkdir` claim. Shares the derivation
 * with `missedRunId` (catchup.ts) so a missed-then-caught-up fire and a live fire
 * for the same UTC slot are one record.
 */
export declare function slotRunId(scheduledFor: Date | string): string;
/**
 * The aligned schedule boundary a fire belongs to: the most recent occurrence of
 * `cron` at or before `at`.
 *
 * This is the occurrence IDENTITY that {@link slotRunId} (forward dispatch) and
 * `missedRunId` (catchup.ts) must both key on. croner's `currentRun()` inside a
 * fire callback is the JITTERED wall-clock trigger instant (it carries
 * milliseconds — verified), not the aligned boundary, so keying `slotRunId`
 * directly on it produced a distinct id per delivery: two callbacks for one
 * occurrence each claimed a different run dir and both launched, and a live fire
 * never collided with its catch-up twin (which keys on the aligned
 * `previousExpectedFire`). Flooring both to this boundary is what makes the
 * single-fire claim a structural claim on `(routine, scheduledFor)` (SING-15).
 *
 * croner's `previousRun()` takes no argument and returns null on a freshly
 * constructed instance, so we walk `nextRun(cursor)` forward from a lookback
 * window and keep the last fire still ≤ `at` — the same derivation catchup's
 * overdue detection has always used.
 */
export declare function alignedSlotForFire(cron: Cron, at: Date): Date | null;
/**
 * Atomically CLAIM a run directory. Returns true on a successful claim, false
 * when the directory already exists (another caller — even in a separate process
 * — owns this (routine, slot) pair). The non-recursive `mkdir` is a single
 * filesystem test-and-set on every POSIX filesystem, the same primitive
 * `claimMissedFire` relies on; it holds across processes where an in-process flag
 * or a released lock cannot.
 */
export declare function claimRunSlot(jobName: string, runId: string): boolean;
/** Discover routine YAML files in a repository's routines/ directory. */
export declare function discoverJobsFromRepo(repoPath: string): Array<{
    name: string;
    path: string;
}>;
/** Check whether a job with the given name exists on disk. */
export declare function jobExists(name: string): boolean;
/**
 * True when `sourcePath` already IS the canonical user-layer file for `name`.
 *
 * `agents routines add <file>` copies a definition into the routines dir, but
 * users routinely point it at the file that already lives there — the release
 * train's own `~/.agents/routines/release-train.yml`. `writeJob` would then
 * re-serialize that file in place and, per {@link serializeJob}, delete every
 * key absent from the canonical output — silently dropping the legacy
 * `devices:` pin from config tracked in the git-backed `~/.agents` repo
 * (RUSH-2517). Callers use this to skip the write when there is nothing to
 * copy.
 *
 * Compares real paths so a symlinked `~/.agents` (the normal layout) still
 * matches, and falls back to a resolved-path compare when either side cannot be
 * realpath'd.
 */
export declare function isCanonicalRoutineSource(sourcePath: string, name: string): boolean;
/** Get the filesystem path of a job's YAML config file, or null if not found. */
export declare function getJobPath(name: string): string | null;
/**
 * Resolve a routine's YAML across EVERY layer `listJobs`/`readJob` read — user
 * then system — not just the user dir.
 *
 * `getJobPath` is user-layer only because its callers write there. Read paths
 * that ask "when did this routine come to exist" need the system layer too:
 * a built-in shipped in the system repo has no user-layer file and no
 * `createdAt`, so a user-layer-only lookup returns null, the overdue floor is
 * skipped, and the routine reads as instantly overdue on first daemon start —
 * exactly the case the floor exists to prevent.
 */
export declare function resolveJobFilePath(name: string): string | null;
/**
 * Parse an "at" time string into a one-shot cron expression.
 * Supports formats like:
 * - "9:00" or "09:00" - today at 9:00 AM (or tomorrow if past)
 * - "14:30" - today at 2:30 PM
 * - "2026-02-24 09:00" - specific date and time
 * Returns null if invalid format.
 */
export declare function parseAtTime(atTime: string): {
    schedule: string;
    runOnce: boolean;
} | null;
/** Check if an installed job's normalized YAML matches the source file. */
export declare function jobContentMatches(name: string, sourcePath: string): boolean;
/** Install a job by reading and validating a YAML source file. */
export declare function installJobFromSource(sourcePath: string, name: string): {
    success: boolean;
    error?: string;
};
/** List all job names that have run directories. */
export declare function listJobsWithRuns(): string[];
/** Count total runs across all jobs. */
export declare function countAllRuns(): number;
/** Preview runs that would be pruned (keeping only the most recent `keep` per job). */
export declare function previewRunsPrune(keep: number): Array<{
    jobName: string;
    runId: string;
    startedAt: string;
}>;
/** Delete old runs, keeping only the most recent `keep` per job. Returns bytes freed and run count. */
export declare function pruneRuns(keep: number): {
    deleted: number;
    bytesFreed: number;
};
