/**
 * Daemon lifecycle management for the routines scheduler.
 *
 * The daemon is a long-running process that holds a JobScheduler and
 * triggers jobs on their cron schedules. It can be managed via launchd
 * (macOS), systemd (Linux), or as a plain detached process. PID tracking,
 * log output, reload (SIGHUP), and graceful shutdown are handled here.
 */
import { isolatedHomeSuffix } from '../service-manifest.js';
import { getAgentsBinPath } from '../cli-entry.js';
import type { ServiceHealth } from './service.js';
/** Health for every service currently registered on the live supervisor, or `null` if the daemon isn't running in this process. */
export declare function getServiceSupervisorHealth(): Record<string, ServiceHealth> | null;
/**
 * RUSH-2639 (residual): launchd/systemd route `unload`/`load`/`list` by the
 * service identifier ALONE, never by the plist/unit file's path. Baking the
 * caller's HOME into the plist content (the earlier RUSH-2639 fix, above)
 * keeps the STARTED daemon inside its sandbox, but every hermetic-test
 * instance and every real interactive install still share the one literal
 * `PLIST_NAME`/`SYSTEMD_UNIT` string. `startDaemonLocked`'s own `unload`
 * before `load` is written to be a no-op ("not loaded, expected") for a
 * plist that has never been loaded — but confirmed on darwin: when a
 * DIFFERENT plist is already loaded under that same label, `unload
 * <this-instance's-own-never-loaded-path>` still tears down the OTHER job
 * (verified directly against real launchctl with two throwaway plists
 * sharing one label — the second job's own `unload` silently kills the
 * first, still alive under a different path). On a machine running several
 * hermetic test forks at once (CI) — or a developer's own suite next to
 * their real always-on daemon — that "other job" is a live daemon with
 * DIFFERENT baked-in state.
 *
 * Namespace the identifier itself whenever HOME has been redirected away
 * from the account's real home. `os.userInfo().homedir` reads the OS/passwd
 * record directly and ignores `$HOME` (unlike `os.homedir()`, which honors
 * it), so comparing the two detects exactly this redirection — true for
 * every hermetic test process, false for every real interactive/production
 * invocation, so a real user's daemon keeps registering under the unchanged
 * production identifier.
 *
 * The rule itself now lives in `service-manifest.ts` — the daemon was the first
 * manifest to need it, not the only one — and is re-exported here because it is
 * part of this module's published surface.
 */
export { isolatedHomeSuffix };
/** launchd Label for this process's daemon — namespaced under a redirected HOME. */
export declare function daemonServiceLabel(): string;
/** systemd --user unit name for this process's daemon — namespaced under a redirected HOME. */
export declare function daemonSystemdUnitName(): string;
/**
 * How many consecutive failed daemon starts disable the *implicit* auto-start
 * (`ensureDaemonStarted`). Matches `DAEMON_START_LIMIT_BURST` so the two layers
 * give up together rather than one silently masking the other. `agents daemon
 * start` is the deliberate override and is never gated by this.
 */
export declare const DAEMON_AUTOSTART_FAILURE_LIMIT = 5;
/**
 * RUSH-1817: decide whether the daemon should (re)take over hosting the secrets
 * broker. The startup host decision is one-shot; this drives the periodic
 * self-heal re-check. Take over ONLY when the daemon is not already hosting AND
 * no healthy broker answers a ping — i.e. a standalone the daemon deferred to at
 * start has since died or crash-looped. Never take over while our in-process
 * broker is hosting, and never clobber a reachable (healthy) broker.
 */
export declare function shouldTakeOverBroker(isHosting: boolean, brokerReachable: boolean): boolean;
/**
 * What a gate re-evaluation must do with the routines scheduler. The daemon
 * re-evaluates `scheduler.enabled` on every SIGHUP reload so flipping the key
 * takes effect without a daemon restart (and `routines add`'s reload signal on
 * a re-enabled box boots the scheduler — the reload is truthful, not a no-op).
 *
 *   running + enabled   → reload (the normal SIGHUP path)
 *   running + !enabled  → stop  (gate flipped off since boot)
 *   !running + enabled  → boot  (gate flipped on since boot)
 *   !running + !enabled → none  (stay dark)
 */
export type SchedulerGateTransition = 'reload' | 'stop' | 'boot' | 'none';
export declare function schedulerGateTransition(running: boolean, enabled: boolean): SchedulerGateTransition;
/**
 * Wrap an async routine so it runs AT MOST ONCE, however many callers fire it.
 *
 * Extracted rather than left as a `let shuttingDown = false` inside runDaemon so
 * the property can actually be tested (RUSH-2423). The daemon's shutdown is
 * reachable from SIGTERM, SIGINT, and the state-dir self-check, but a real
 * shutdown completes in ~26ms, so the re-entrant window is not reachable from
 * outside the process — an end-to-end "send three signals" test passes with the
 * guard removed and proves nothing. The mechanism is what is testable, so the
 * mechanism is what is separated out.
 *
 * The flag is set synchronously before the first `await`, which is what makes
 * this safe: two callers in the same tick cannot both get past it.
 *
 * A rejected `fn` leaves the guard SET — one attempt is all there is, and the
 * rejection propagates to the caller that made it. That is right for shutdown
 * (a failed shutdown must not be silently retried by the next signal) but is
 * the thing to re-examine before giving this a second consumer.
 */
export declare function singleShot(fn: () => Promise<void>): () => Promise<void>;
/**
 * Absolute path to the daemon's structured log.
 *
 * Exported because two commands rebuilt the same path from a hardcoded
 * `'logs.jsonl'` literal (`commands/daemon.ts`, `commands/routines.ts`), so
 * renaming the file would have silently pointed them at nothing (RUSH-2423).
 * One definition, three callers.
 */
export declare function getDaemonLogPath(): string;
/** Read the stored daemon PID from disk. Returns null if not present or invalid. */
export declare function readDaemonPid(): number | null;
/** Write the daemon PID to the pid file. */
export declare function writeDaemonPid(pid: number): void;
/** Remove the daemon PID file. */
export declare function removeDaemonPid(): void;
export interface DaemonHeartbeat {
    lastTick: string;
    pid: number;
}
export declare function writeHeartbeat(pid?: number): void;
export declare function readHeartbeat(): DaemonHeartbeat | null;
export declare function removeHeartbeat(): void;
export declare function isDaemonWedged(): boolean;
/**
 * Check whether a daemon is alive — via the pid file, or a fresh heartbeat when
 * the pid file has been lost (see resolveLiveDaemonPid). The observation itself
 * is read-only; when it finds desync it opportunistically acquires daemon.lock
 * and repeats the observation there before repairing. A contended probe still
 * returns the observed liveness without mutating another lifecycle operation's
 * state.
 */
export declare function isDaemonRunning(): boolean;
/**
 * Single-instance claim for the daemon foreground entrypoint.
 *
 * `agents __daemon-run` is reachable directly — a manual invocation, or a
 * service-manager restart that races a still-alive predecessor — bypassing the
 * start lock in startDaemon(). Without this guard runDaemon() would call
 * writeDaemonPid() unconditionally, clobber a live daemon's recorded PID, and
 * run a second JobScheduler concurrently, so every cron routine fires twice.
 *
 * LAST-WINS takeover (SING-11, RUSH-2352): when a live daemon already owns the
 * pid file, this does NOT defer to it — it evicts the incumbent and becomes the
 * survivor, so a second install can never leave two daemons running. Returns true
 * and records our PID once the incumbent is provably dead (its resources
 * released). Returns false when another `__daemon-run` currently holds the
 * O_EXCL start lock, or when the incumbent cannot be safely identified/evicted,
 * in which case the caller must exit without touching further state.
 * The read-evict-write is serialized behind the same start lock startDaemon()
 * uses, so two `_run` processes can't both claim in the window between the
 * liveness check and the write.
 */
export declare function claimDaemonInstance(): boolean;
/**
 * Record this daemon in the device's instance registry — a marker file named by
 * pid under `<daemonDir>/instances/`. The registry, not a process scan, is how
 * the reaper enumerates the device singleton: because the dir lives INSIDE the
 * state dir (`AGENTS_DAEMON_DIR` ?? `<HOME>/.agents/.cache/helpers/daemon`), every
 * daemon of one device — however it was launched — registers in the same place,
 * while a genuinely separate install/home or a test fixture registers under its
 * own state dir and is invisible here. This is what fixes the two-entry pile-up:
 * the compiled `dist/bin/agents` binary and the `node <shim>` JS entry have
 * different `process.argv[1]`, so the old launch-entry-scoped `ps` match never
 * reaped across them and duplicates accumulated (78 observed on one box), every
 * routine double-firing. Best-effort — the reaper self-heals a missing/stale
 * marker, and reading another process's ENV to key on the state dir directly is
 * not portable (hardened macOS hides it from `ps`), so identity rides the shared
 * on-disk registry instead. No-op on Windows (POSIX-only reaper).
 */
export declare function registerDaemonInstance(pid?: number): void;
/** Remove this daemon's registry marker on graceful shutdown. */
export declare function unregisterDaemonInstance(pid?: number): void;
/**
 * Reap stray duplicate daemons of THIS device — every registrant in the instance
 * registry that is a live `agents __daemon-run` and is neither this process nor
 * the current pid-file owner. A predecessor SIGKILLed/OOM-ed without cleanup, or a
 * duplicate that lost the pid-file write race, would otherwise keep a second
 * scheduler alive and double-fire jobs even after claimDaemonInstance() hands the
 * pid file to the survivor. Also garbage-collects markers whose pid is dead or was
 * reused by an unrelated process. No-op on Windows (POSIX-only).
 */
export declare function reapStrayDaemons(keepPid?: number): {
    reaped: number;
    details: string[];
};
export declare function isLiveDaemon(pid: number): boolean;
/** Append a JSONL log entry to the daemon log file (owner-only permissions). */
export declare function log(level: string, message: string): void;
/** Keep synchronous signal-handler failures inside the daemon's crash barrier. */
export declare function guardSignalHandler(handler: () => void, onError: (err: unknown) => void): () => void;
/** Main daemon loop: load jobs, schedule crons, monitor runs, and handle signals. */
/**
 * Anchor the daemon's working directory to a stable, always-present path.
 *
 * The daemon is long-lived and inherits whatever cwd it was launched from — often
 * a git worktree (e.g. a `.agents/worktrees/<slug>/` a session happened to be in).
 * When that directory is later removed (`git worktree remove`, `rm -rf`), the
 * daemon keeps the deleted inode as its cwd — a process cannot chdir out of a
 * deleted directory on its own — and every job it spawns inherits the dead cwd
 * (`spawnJobAttempt` and command runs pass no explicit `cwd`, so the child uses
 * the parent's). Bun then fails `getcwd()` during startup and every routine crashes
 * at 0 seconds with `ENOENT: Bun could not find a file` before the agent even runs.
 *
 * Re-anchoring to the home directory once, at daemon startup, makes the daemon
 * immune regardless of how it was launched (systemd unit, launchd, or a manual
 * `agents __daemon-run` from any directory). Returns the resolved cwd, or null if
 * anchoring failed (logged, non-fatal).
 */
export declare function anchorDaemonCwd(): string | null;
/**
 * Surface, at the daemon's OWN startup, that it was launched from an ephemeral
 * root that will wedge it if the directory is removed. This is the runtime
 * companion to the launch-time check in validateDaemonBinary (which only runs
 * when the daemon is *spawned* via getDaemonLaunch): a direct
 * `agents __daemon-run` from a temp or worktree build — e.g. a review/verify
 * checkout under /tmp — never passes through that path, so without this the
 * wedge risk stays invisible until jobs start ENOENT-ing on their dynamic
 * imports. Best-effort and non-fatal; the cwd is already handled by
 * anchorDaemonCwd, but a deleted module root can only be flagged, not repaired.
 *
 * `resolveBin` is injectable (defaults to getAgentsBinPath) so the wiring — the
 * predicate call, the WARN, and the non-fatal guard around a throwing resolver —
 * is testable. Returns the warning message it logged, or null when the launch
 * root is stable (or could not be resolved).
 */
export declare function warnEphemeralDaemonRoot(resolveBin?: () => string): string | null;
/**
 * Test-home tripwire (PHNX-2545). The routines/daemon test suite spawns real
 * `agents __daemon-run` processes against an isolated /tmp HOME. If that HOME
 * override fails to reach the child — an `env: {...process.env}` spawn that
 * forgot to set it, a login shell that reset HOME — the daemon resolves its
 * state dir under the operator's REAL home and its scheduler/watchdog then tick
 * against shared production state. That is the exact leak the ticket reports:
 * real test daemons found alive on a fleet box, each a second live scheduler
 * racing the legitimate one, in violation of the execution-singularity spec.
 *
 * A test that spawns a daemon sets AGENTS_DAEMON_TEST_HOME to the isolated home
 * it provisioned. When that marker is present, this daemon's resolved state dir
 * MUST sit under it; otherwise the daemon refuses to boot — failing loud before
 * it claims an instance, writes a pid, or fires a single tick (the throw is
 * caught in index.ts's `__daemon-run` handler, logged, and exits non-zero) —
 * rather than running in the wrong directory against the real host. In
 * production the marker is never set, so this is a no-op there.
 *
 * `daemonDir`/`testHome` are injectable so the pure guard is unit-testable
 * without spawning a process; the defaults read the live daemon dir and env.
 */
export declare function assertTestDaemonHome(daemonDir?: string, testHome?: string | undefined): void;
export declare function runDaemon(): Promise<void>;
/**
 * Write a launchd plist or systemd unit with owner-only permissions atomically.
 *
 * `writeFileSync`'s `mode` is honored only when the file is *created*, so we
 * unlink any pre-existing manifest first. That guarantees every write is a
 * fresh 0600 create — closing the TOCTOU window on new files AND re-locking a
 * stale world-readable manifest left by an older install — since these files
 * embed long-lived credentials.
 */
export declare function writeOwnerOnlyServiceManifest(filePath: string, content: string): void;
/**
 * Generate a macOS launchd plist for auto-starting the daemon.
 *
 * The plist never embeds a Claude OAuth token: the daemon holds no Claude
 * credential at all. Routine runs authenticate through the per-account
 * CLAUDE_CONFIG_DIR login on this device, exactly like an interactive
 * `agents run`, so no credential ever touches the service manifest.
 *
 * RUSH-2639: launchd does NOT inherit `launchctl load`'s caller's process
 * environment — a spawned daemon only ever sees the login session's default
 * env plus whatever this dict adds/overrides. Before this fix the dict carried
 * only PATH, so HOME resolved to the launchd session's own value regardless of
 * what HOME the process that generated (and loaded) the plist was running
 * under. In production that's a no-op (the login session's HOME already is the
 * real HOME), but under a hermetic test harness that redirects HOME to a
 * fork-private sandbox, a launchd-started daemon silently escaped the sandbox
 * and bootstrapped `~/.agents` (.cache/.history/.system/routines) in the
 * developer's/runner's REAL home. Baking HOME (and the AGENTS_REAL_HOME seam
 * every version-home consumer honors, see tests/setup.ts) into the plist at
 * generation time makes the launchd child inherit the SAME home the caller
 * resolved, exactly like the plain detached-spawn path already does via
 * `env: {...process.env}`.
 */
export declare function generateLaunchdPlist(agentsBin?: string): string;
/**
 * Generate a Linux systemd user unit for auto-starting the daemon.
 *
 * The unit never embeds a Claude OAuth token: the daemon holds no Claude
 * credential at all. Routine runs authenticate through the per-account
 * CLAUDE_CONFIG_DIR login on this device, exactly like an interactive
 * `agents run`, so no credential ever touches the unit file.
 *
 * RUSH-2639: same seam as `generateLaunchdPlist` — a systemd --user unit is
 * started by the user's systemd instance, not the process that generated the
 * unit, so HOME is whatever that session provides unless this file pins it.
 * Baking HOME (and AGENTS_REAL_HOME) in at generation time keeps a
 * hermetic-test-started unit inside its sandbox instead of resolving against
 * the real account home.
 */
export declare function generateSystemdUnit(agentsBin?: string): string;
export { getAgentsBinPath };
/** Start the daemon via launchd, systemd, or as a detached process. */
export declare function startDaemon(agentsBin?: string): {
    pid: number | null;
    method: string;
};
/**
 * Is the auto-start circuit breaker open (RUSH-2418)? True once
 * {@link DAEMON_AUTOSTART_FAILURE_LIMIT} consecutive starts have failed to
 * produce a daemon that reported healthy. Pure read of the persisted health
 * record, and `agents daemon doctor` reports the same record — the message the
 * breaker prints has to lead somewhere that can explain it.
 */
export declare function isDaemonAutostartCircuitOpen(): boolean;
/**
 * Bring the always-on daemon up as a side effect of a background-adjacent
 * command (secrets unlock, browser start, ...), not only from `routines add`.
 *
 * Delegates to the single `startDaemon` entrypoint, so it honors the
 * single-instance start lock and is a no-op when a daemon is already running
 * (returns `already-running`). Best-effort: any failure is swallowed and null
 * returned, so ensuring the daemon can never break the foreground command that
 * happened to bring it up. See issue #415.
 */
export declare function ensureDaemonStarted(): {
    pid: number | null;
    method: string;
} | null;
/**
 * Resolve how to launch the daemon: `node <entry> __daemon-run`, matching the
 * exact form that works under a direct `__daemon-run`.
 *
 * We spawn the Node runtime (`process.execPath`) with the CLI entry as an
 * argument rather than executing the entry path directly. Executing the `.js`
 * path relies on its shebang on POSIX, and on Windows CreateProcess can't run a
 * `.js`/shim directly at all — it gets launched through a transient
 * console-owning wrapper (cmd.exe / the npm shim). When that wrapper exits it
 * closes its console, and the detached daemon sharing that console receives a
 * console-close event that trips its shutdown handler — the daemon comes up,
 * binds the browser IPC socket, then tears itself down ~36ms later (#556).
 * Going through `process.execPath` means a real PE/binary is spawned with
 * `detached: true` and no console, so nothing signals the daemon after launch.
 *
 * When the entry isn't a Node script (e.g. a native compiled launcher), run it
 * directly — it owns its own runtime resolution.
 */
export declare function getDaemonLaunch(agentsBin?: string): {
    command: string;
    args: string[];
};
/**
 * Build the argv to relaunch the `agents` CLI with the given subcommand args.
 *
 * Resolves the real on-disk binary via getAgentsBinPath(), then dispatches: a
 * `.js` entry runs under node (`node <entry> …`), a native/compiled binary runs
 * directly (`<bin> …`).
 *
 * Callers MUST route self-spawns through this rather than hand-rolling
 * `[process.execPath, process.argv[1], …]`: under the compiled standalone binary
 * (#315) `process.argv[1]` is the bun virtual entry `/$bunfs/root/agents`, so the
 * hand-rolled form becomes `agents /$bunfs/root/agents …` → the CLI receives the
 * bunfs path as a subcommand and dies with "unknown command '/$bunfs/root/agents'".
 * getAgentsBinPath() resolves that virtual entry to the physical process.execPath.
 */
export declare function getAgentsInvocation(subArgs: string[], agentsBin?: string): {
    command: string;
    args: string[];
};
/**
 * A daemon binary living under an ephemeral path — a git worktree, or a temp
 * directory (`/tmp`, `/var/folders`, `/dev/shm`) — is a latent wedge. The daemon
 * is long-lived but resolves its own job modules by dynamic `import()` rooted at
 * this entry (getAgentsBinPath → process.argv[1]). If that directory is later
 * removed (`git worktree remove`, a `/tmp` cleanup, a review/verify checkout
 * teardown) the running daemon keeps ENOENT-ing on every job it loads —
 * `anchorDaemonCwd` rescues the cwd, but nothing can re-root a deleted module
 * tree. Returns a human phrase naming the ephemeral kind, or null for a stable
 * install path (version home, a global npm prefix, a normal source checkout).
 */
export declare function describeEphemeralDaemonRoot(binPath: string): string | null;
export declare function validateDaemonBinary(binPath: string): {
    warnings: string[];
};
interface StartDetachedOptions {
    /** CLI entry to launch (defaults to the running binary). Injectable for tests. */
    agentsBin?: string;
    /** Log file the daemon's stdio is redirected to (defaults to the daemon log). */
    logPath?: string;
    /** Environment for the child (defaults to the daemon's current process env). */
    env?: NodeJS.ProcessEnv;
}
export declare function startDetached(opts?: StartDetachedOptions): {
    pid: number | null;
    method: string;
};
/**
 * One piece of daemon state that a graceful `handleShutdown` removes and an
 * escalated kill leaves behind (RUSH-2421).
 */
export interface StopResidueArtifact {
    label: string;
    present: boolean;
    /** The file names a live owner, including a stopped target that survived. */
    ownedByLiveOther: boolean;
    reclaim: () => void;
    stillPresent: () => boolean;
}
/**
 * The lifetime marker, heartbeat, and instance-registry entry, described so
 * {@link stopDaemon} can assert them the same way it asserts the two sockets.
 *
 * Ownership, not mere presence, decides: a file naming a pid that is alive and
 * is not the daemon we just stopped belongs to a DIFFERENT daemon (a successor
 * that started during the stop, or a peer serving this state dir), and deleting
 * it would break that live daemon — the same reasoning the broker-socket branch
 * above uses for a standalone owner. Everything else is residue from a provably
 * dead owner and is reclaimed.
 */
export declare function stopResidueArtifacts(stoppedPid: number | null, survivors?: number[]): StopResidueArtifact[];
/**
 * Structured outcome of {@link stopDaemon} (SING-12, RUSH-2355). `stopDaemon`
 * asserts its postcondition instead of assuming it: `ok` is true only when every
 * resource the daemon held is provably released. `surviving` names anything that
 * did not release (a still-live daemon, or a stale socket that could not be
 * cleared) and is what drives a non-zero exit; `detachedChildren` are the
 * in-flight routine children that survive deliberately (SING-11a) and are
 * reported, never killed.
 */
export interface DaemonStopResult {
    ok: boolean;
    stoppedPid: number | null;
    escalated: boolean;
    released: string[];
    surviving: string[];
    detachedChildren: number[];
}
export declare function findSurvivingStateDirDaemons(exclude: Set<number>): number[];
/**
 * Stop the daemon and ASSERT its postcondition (SING-12, RUSH-2355), unloading it
 * from launchd/systemd if applicable.
 *
 * The SIGTERM → grace → killTree sequence is unchanged; what it adds is
 * verification. After the daemon is gone it checks that the secrets broker socket
 * and browser IPC binding actually released — a stale socket present on disk but
 * with no live owner is the orphan that keeps clients holding unlocked bundles
 * hanging (`daemon.ts` broker-hosting; the two-brokers-on-one-socket bug) — and
 * that no `__daemon-run` for THIS state dir survives. A killTree escalation exits
 * without running the daemon's graceful handleShutdown, so those sockets can be
 * left stale; this reclaims each (the owner is provably dead) and reports it. It
 * never reports success on an unverified stop.
 */
export declare function stopDaemon(): DaemonStopResult;
/** Get current daemon status including running state, PID, and enabled job count. */
export declare function getDaemonStatus(): {
    state: 'running' | 'wedged' | 'stopped';
    running: boolean;
    pid: number | null;
    jobCount: number;
    logPath: string;
    binaryPath: string | null;
    heartbeat: DaemonHeartbeat | null;
};
/** Read the daemon log, optionally limited to the last N lines. */
export declare function readDaemonLog(lines?: number): string;
/** Send SIGHUP to the daemon to trigger a job reload. */
export declare function signalDaemonReload(): boolean;
