/**
 * Install + lifecycle for the macOS menu-bar helper (`MenubarHelper.app`).
 *
 * Mirrors `src/lib/secrets/install-helper.ts` (stable Application Support path,
 * survives npm re-sign) and the secrets-agent launchd pattern in
 * `src/lib/secrets/agent.ts` (RunAtLoad + KeepAlive user service).
 *
 * The helper is a no-Dock `.accessory` status-bar app. It reads live agent
 * state directly from disk and shells `agents` only for actions, so the plist
 * bakes in the node interpreter + entry point + bin path so the GUI process can
 * find the CLI without a login PATH.
 *
 * Opt-out is sticky: `agents menubar disable` drops a sentinel that the upgrade
 * migration (`installMenubarLaunchAgent` in migrate.ts) honors, so a disabled
 * menu bar never silently comes back on the next release.
 */
/**
 * The bundled executable's basename (RUSH-3101) — what launchd execs directly
 * inside `Contents/MacOS/`, bypassing LaunchServices name resolution. Before
 * this it was `MenubarHelper`, so that raw Mach-O basename is exactly what
 * macOS showed in System Settings > Privacy & Security > Accessibility and in
 * the "would like to control this computer" prompt. Renaming this constant
 * alone does NOT touch the bundle id, Team ID, or designated requirement
 * (`SERVICE_LABEL_BASE`, `MENUBAR_HELPER_BUNDLE_ID` in download-menubar.ts) —
 * those are what keep the existing Accessibility grant alive across upgrades.
 * Every basename-matching check reads this constant rather than re-deriving
 * the string; the Swift side has its own equal in
 * `menubar/Sources/MenubarHelper/HelperIdentity.swift`.
 */
export declare const MENUBAR_HELPER_EXECUTABLE_NAME = "AGI Menu";
/**
 * launchd Label for this process's helper — the production identifier for a
 * real invocation, namespaced under a redirected HOME (RUSH-2639). launchd
 * routes bootout/bootstrap/kickstart by identifier alone, so without this a
 * hermetic test fork's own teardown boots out the operator's live helper.
 */
export declare function serviceLabel(): string;
/**
 * What the installed helper IS — deliberately not the CLI's version.
 *
 * `source` matters because the two install paths have different notions of
 * "changed": a release bundle is identified by its helper version, while a local
 * dev build has none (menubar/scripts/build.sh hardcodes CFBundleShortVersionString),
 * so it is identified by the source path + mtime it was copied from.
 */
/** Version label for a bundle that has none of its own (a local dev build). */
export declare const LOCAL_BUILD_LABEL = "local";
export type MenubarStamp = {
    source: 'release';
    helperVersion: string;
} | {
    source: 'local';
    sourceStamp: string;
} | {
    source: 'legacy';
    raw: string;
};
/**
 * Absolute path to the installed menu-bar helper executable if it exists on
 * disk, else null. The desktop notifier (notify-desktop.ts) routes daemon
 * notifications through this one-shot (`"AGI Menu" --notify ...`) so they
 * carry the agents-cli mark rather than the generic osascript icon. Null on
 * non-darwin or when the helper is not installed (menu bar disabled, a Linux
 * package, or a dev checkout without a built bundle).
 */
export declare function resolveInstalledMenubarExecutable(): string | null;
/** True if the user explicitly disabled the menu bar (don't auto-enable on upgrade). */
export declare function menubarDisabledByUser(): boolean;
/** True if the launchd plist for the menu-bar service is installed. */
export declare function menubarServiceInstalled(): boolean;
/** True when the bundle carries a signature the kernel will accept at launch. */
export declare function codesignVerifies(appPath: string): boolean;
/**
 * True when Gatekeeper will let the bundle execute on this machine.
 * A Developer-ID-signed but un-notarized app is rejected by `spctl --assess`,
 * which macOS surfaces as "the app is damaged" and can crash AppKit during
 * launch. This is separate from `codesign --verify`: a signature can be valid
 * while Gatekeeper still refuses to run it. The release notarizes + staples the
 * helper (menubar/scripts/build.sh, gated by verify-menubar-helper.sh), so a
 * shipped bundle passes this; the launch guards use it to fail loud rather than
 * bootstrap a helper macOS would reject.
 */
export declare function gatekeeperAssesses(appPath: string): boolean;
/** True when the bundle carries a Developer ID TeamIdentifier (not ad-hoc). */
export declare function hasDeveloperIdSignature(appPath: string): boolean;
/**
 * Copy the bundled `.app` to the stable user path (idempotent unless forced).
 * Returns the installed executable path, or null if no source bundle ships
 * with this install (e.g. Linux package, or a build without the helper).
 *
 * Also heals an older install that was ad-hoc re-signed over a Developer ID
 * source: that unstable identity made Accessibility re-prompt on every upgrade.
 * When the shipped source is Developer ID and the installed copy is only
 * ad-hoc, replace it — even without forceReinstall.
 */
export declare function ensureMenubarAppInstalled(opts?: {
    forceReinstall?: boolean;
    sourceAppPath?: string;
}): string | null;
export declare function generateServicePlist(execPath: string): string;
/**
 * Restart the menu-bar launchd agent from a clean state.
 *
 * Always `bootout` first: on modern macOS `bootstrap` fails when the job is
 * already bootstrapped, and the deprecated `load -w` fallback is unreliable.
 * A prior WindowServer disconnect can leave the job throttled so that a plain
 * `kickstart -k` does not bring it back; booting it out and bootstrapping fresh
 * is the only sequence that reliably re-attaches the status item.
 */
export declare function restartMenubarLaunchAgent(uid: number, plist: string, exec?: (cmd: string, args: readonly string[], opts: {
    stdio: ['ignore', 'ignore', 'ignore'];
}) => Buffer): void;
/**
 * Force a running helper off the binary that was just swapped underneath it.
 *
 * `installMenubarLaunchAgentOnUpgrade()` calls this after a heal actually
 * replaces the on-disk bundle (a version bump or the ad-hoc -> Developer ID
 * transition — see `menubarHealReplacedBundle`). `restartMenubarLaunchAgent`
 * (run moments earlier via `installAndStartService`) already attempts
 * bootout+bootstrap+kickstart, but that targets the GUI launchd domain and
 * fails silently — `launchctl kickstart -k gui/<uid>/<label>` prints "Could not
 * find service ... in domain for user gui" — from a shell with no Aqua session,
 * which is the ordinary case for `agents` invoked from a terminal/tmux/ssh
 * session rather than literally the login item (verified live). When that
 * happens the plist and on-disk bundle are current but the LIVE process is
 * still the old binary, so it requests Accessibility under the OLD code
 * identity and the grant never sticks.
 *
 * `kickstart -k` (force-restart, unlike the bare kickstart already tried) is
 * attempted first since it is the clean launchd-native path when it works. If
 * launchd has no record of the service in this context either, fall back to
 * ending the specific pid(s) already confirmed to be running the installed
 * bundle (`ownProcesses`, resolved by the caller via `liveMenubarProcesses`) —
 * never a broad pkill. The launchd plist has `KeepAlive=true`, so the ended
 * process is relaunched from the swapped binary within seconds.
 */
export declare function restartMenubarHelperAfterSwap(uid: number, ownProcesses: readonly MenubarProcess[], exec?: (cmd: string, args: readonly string[], opts: {
    stdio: ['ignore', 'ignore', 'ignore'];
}) => Buffer, kill?: (pid: number) => void): void;
/**
 * Install + start the menu-bar helper as a launchd user service (idempotent).
 * Clears the sticky opt-out, installs the .app, writes the plist, and
 * bootstraps it into the GUI domain. Returns false on non-darwin or when no
 * helper bundle can be resolved for this install.
 *
 * ASYNC and download-capable: this is the EXPLICIT user-initiated path
 * (`agents menubar enable`). When no bundled/local `.app` ships (a fresh
 * `npm i -g` machine whose tarball lacks the bundle), it fetches the signed +
 * notarized release asset for the running CLI version — verified (sha256 +
 * codesign + Team + designated-requirement pin + notarization) before install
 * — and starts the service from that cached copy. The startup self-heal never
 * routes here: it calls `startMenubarServiceFromSource` and stays sync +
 * network-free, no-opping when no local source exists.
 */
export declare function enableMenubarService(opts?: {
    clearOptOut?: boolean;
}): Promise<boolean>;
/**
 * Render a stamp as a comparable/displayable version string.
 *
 * A local build has no version of its own, so it reports `local`; the ownership
 * contest treats that as "not comparable" and falls through to its owner arm,
 * which is the correct outcome — a dev build must never win a version contest
 * against a release.
 */
export declare function stampVersionLabel(stamp: MenubarStamp | null): string | null;
/**
 * Identify the bundle about to be installed, for the stamp.
 *
 * A bundle is a RELEASE iff it sits under the helper's own download cache —
 * asked of `menubarHelperCacheDir`, not pattern-matched out of the path. A regex
 * for `/v<x.y.z>/` gets this wrong in both directions: a checkout living under
 * any directory that happens to contain a version-shaped segment reads as a
 * release, and a release cache laid out differently reads as local. Either
 * misclassification flips `source` between invocations, and a kind change is
 * unconditionally stale — which is a reinstall loop.
 */
export declare function stampFor(resolvedSourceAppPath: string): MenubarStamp;
/**
 * The helper version a path denotes, iff it is inside that version's cache dir.
 * Returns null for anything else — including a version-shaped path that is not
 * actually the cache.
 */
export declare function releaseVersionOfCachedBundle(appPath: string, cacheDirFor?: (v: string) => string): string | null;
/**
 * Pure staleness decision (no I/O) so the truth table is unit-testable.
 *
 * This compares the HELPER axis, not the CLI's. It used to compare the installed
 * stamp against `getCliVersion()`, which was wrong in both directions once the
 * helpers gained their own version line: every CLI release made an unchanged
 * helper look stale and reinstalled it (the #2109 restart storm), while a
 * genuinely newer helper at the same CLI version never looked stale at all.
 *
 * Stale when: the executable is gone; nothing is stamped; the stamp predates the
 * JSON format (`legacy` — re-stamped once); the install KIND changed
 * (local <-> release), since a dev build and a release bundle are not
 * interchangeable; a release install whose available helper version is newer;
 * or a local install whose source path or mtime moved.
 */
export declare function isMenubarStale(opts: {
    installed: MenubarStamp | null;
    available: MenubarStamp;
    execExists: boolean;
}): boolean;
/**
 * Pure decision (no I/O): did THIS heal actually replace bundle content a live
 * process could be running stale, as opposed to a plist-only repoint?
 *
 * `stale` (version bump or missing exec) and `needsDevIdHeal` (ad-hoc ->
 * Developer ID) both mean the shipped `.app` differs from what is already
 * installed — the exact condition under which a running helper can be left on
 * an old code identity after `enableMenubarService` swaps the bundle. A
 * repoint-only heal (`menubarSetupNeedsRepoint()` alone, same version, same
 * signing identity) does NOT count: RUSH-3005 already owns the churn from
 * mixed-Node-interpreter repoints, and restarting the helper on every one of
 * those would double that churn rather than fix this bug.
 */
export declare function menubarHealReplacedBundle(opts: {
    stale: boolean;
    needsDevIdHeal: boolean;
}): boolean;
/**
 * Pure re-point decision (no I/O): the plist's baked interpreter/entry no longer
 * match the install that is now running `agents`. This catches DUAL-INSTALL skew
 * that a version bump can't — e.g. the plist was baked by an nvm copy but the
 * user's `agents` now resolves to a bun copy (same or different version), so the
 * helper keeps shelling the stale install for its menu data AND the quick-issue
 * dispatch. A null active entry (a dev/tsx run where the compiled entry can't be
 * resolved) never triggers a re-point, so it can't churn the plist onto a
 * transient path.
 */
export declare function menubarPlistNeedsRepoint(opts: {
    plistEntry: string | null;
    plistNode: string | null;
    plistNodeExists: boolean;
    activeEntry: string | null;
    activeNode: string | null;
}): boolean;
/**
 * Stop + remove the menu-bar service and write the sticky opt-out so the
 * upgrade migration won't re-enable it.
 */
export declare function disableMenubarService(): void;
/**
 * Which install is allowed to (re)install the shared helper.
 *
 * The helper lives at ONE path in Application Support, but any number of
 * agents-cli copies can be present on a box and every one of them runs the
 * startup self-heal. The version stamp and the plist's baked `AGENTS_ENTRY` each
 * record whichever copy acted last, so without an ownership rule every copy
 * reads the others' marks as drift and recopies the bundle over them. Recopying
 * replaces the executable under the live helper and kills it; launchd
 * `KeepAlive` restarts it; the next copy repeats it. Measured on one box: a new
 * pid every 5-15s, 578 launches in the helper's log, a status item that never
 * stayed visible, and `agents menubar status` still reporting `running: yes`
 * because a pid always existed (#2109).
 *
 * There is deliberately NO content comparison here. Comparing the shipped helper
 * against the installed one cannot distinguish "real upgrade" from "another
 * install's copy": the helper is rebuilt, re-signed and re-notarized on every
 * release (`menubar/scripts/build.sh` via `release.sh`), so consecutive releases
 * ship byte-different bundles from identical Swift source — 1.22.20/21/22 all
 * have the same 2876288-byte executable and three different sha256s AND three
 * different CDHashes. Any digest gate therefore reports "changed" for exactly
 * the skew case it was meant to exempt.
 *
 * So the installed version decides release skew: a newer signed release takes
 * ownership immediately, an older release cannot downgrade it, and equal foreign
 * releases retain the recorded owner. The plist's `AGENTS_ENTRY` remains the
 * ownership signal for legacy state with no version marker, where the cooldown
 * bounds takeover churn. Pure so the truth table is unit-testable.
 */
export declare function mayInstallMenubarHelper(opts: {
    /** `AGENTS_ENTRY` baked into the installed plist — the recorded owner. */
    plistEntry: string | null;
    /** `resolveCliEntry()` for the install now running `agents`. */
    activeEntry: string | null;
    /** Whether `plistEntry` still exists on disk. */
    ownerEntryExists: boolean;
    /** The App Support helper executable is absent — a repair, not a contest. */
    helperExecMissing: boolean;
    /** Installed copy is ad-hoc while the shipped source is Developer ID. */
    needsDevIdHeal: boolean;
    /** Version stamped beside the installed helper, or null for legacy state. */
    installedVersion: string | null;
    /** Version of the agents-cli install now attempting the heal. */
    currentVersion: string | null;
    /** ms since the last self-heal reinstall, or null if none is recorded. */
    msSinceLastHeal: number | null;
    /** How long a non-owner waits before it may take over. */
    cooldownMs: number;
    /** This install's OWN shipped bundle is Developer-ID signed (not ad-hoc/dev). */
    sourceIsDeveloperId: boolean;
}): boolean;
/**
 * Startup self-heal, run on every darwin CLI invocation (see src/index.ts).
 * No-ops cheaply (a couple of existsSync + a tiny file read) unless work is
 * needed:
 *   - fresh install (no service yet)      -> enable
 *   - upgrade (version stamp changed) or  -> re-enable: recopy the new helper
 *     the App Support helper went missing     binary + rewrite the plist + kick
 *
 * Without the staleness re-enable, `npm update` refreshed the CLI but left the
 * menu bar running the previous release's helper binary on a possibly-stale
 * plist. Everything past the ownership gate is unchanged; the gate is what stops
 * coexisting installs reinstalling over each other forever (#2109). No-ops if:
 * not darwin, the user opted out, or no helper bundle ships. Best-effort — never
 * throws into startup.
 */
export declare function installMenubarLaunchAgentOnUpgrade(): void;
/**
 * Pure decision (no I/O): run the one-time `tccutil reset Accessibility` only
 * on a real ad-hoc -> Developer ID transition, and only once ever. A machine
 * that was always Developer ID never accumulated a dead TCC row under an
 * ad-hoc identity, so resetting there would just force a needless re-prompt;
 * a machine that already migrated must not be reset again on every heal.
 */
export declare function shouldMigrateMenubarTcc(opts: {
    needsDevIdHeal: boolean;
    alreadyMigrated: boolean;
}): boolean;
/**
 * Reset the stale Accessibility grant TCC recorded against the pre-migration
 * ad-hoc identity, then stamp the marker so this never runs again on this
 * machine. Targets `SERVICE_LABEL_BASE` — the bundle's actual
 * `CFBundleIdentifier`/codesign `--identifier` (menubar/scripts/build.sh) that
 * TCC keys grants to — not the namespaced `serviceLabel()`, which exists only
 * to keep launchd job labels apart under a redirected HOME (RUSH-2639) and is
 * never the app's real identity. Best-effort: a missing/failing `tccutil`
 * (older macOS, a sandboxed test HOME) must never block the heal that just
 * fixed the signing identity itself.
 */
export declare function resetMenubarAccessibilityTcc(exec?: (cmd: string, args: readonly string[], opts: {
    stdio: ['ignore', 'ignore', 'ignore'];
}) => Buffer): void;
/** One step of `agents menubar setup`, and how it came out. */
export interface SetupStep {
    /** What was configured. */
    name: string;
    /** `ok` — already correct or now correct; `changed` — this run fixed it;
     *  `failed` — could not be configured (setup reports and exits nonzero). */
    outcome: 'ok' | 'changed' | 'failed';
    detail: string;
}
export interface SetupResult {
    steps: SetupStep[];
    /** Every step landed on `ok`/`changed` and exactly one helper is running. */
    configured: boolean;
    status: MenubarStatus;
}
/**
 * Decide which live helper processes must be ended so exactly one status item
 * survives. Pure so the choice is unit-testable without a live menu bar.
 *
 * EVERY current process is ended, including the wanted one: the caller
 * re-kickstarts the launchd service straight after, so the survivor is the one
 * launchd owns (RunAtLoad + KeepAlive), not whichever copy happened to win a
 * race. Picking a survivor from a `ps` listing cannot do this — the list says
 * nothing about which pid launchd will keep alive, so leaving one alive risks
 * keeping the un-managed copy and re-creating the duplicate on next login.
 */
export declare function processesToEnd(status: Pick<MenubarStatus, 'instances' | 'foreignInstances'>): MenubarProcess[];
/**
 * `agents menubar setup` — configure the menu bar end-to-end, idempotently.
 *
 * The one command that gets a machine to the intended state: exactly one status
 * item, owned by a launchd service that starts it at login and restarts it if it
 * dies. Each concern is a reported step, so a partial failure names itself
 * instead of hiding behind "enabled".
 *
 *   1. bundle      — install/refresh the .app at the stable App Support path
 *   2. signature   — a valid code identity (macOS 26+ SIGKILLs an invalid one)
 *   3. duplicates  — end every live helper, so the only survivor is launchd's
 *   4. login item  — write the plist (RunAtLoad + KeepAlive) and bootstrap it
 *   5. single      — verify exactly one helper came back up
 */
export declare function runMenubarSetup(): Promise<SetupResult>;
/** A live MenubarHelper process: its pid and the executable it is running. */
export interface MenubarProcess {
    pid: number;
    executable: string;
}
/**
 * Split the live MenubarHelper processes into the installed bundle's own
 * (`own`) and every other copy (`foreign`).
 *
 * `pgrep -f MenubarHelper` conflated the two, so a stray dev build could hold
 * the global Cmd-Shift-V chord (RegisterEventHotKey is first-come) while status
 * still reported a healthy `running: yes` — the paste was dead and nothing said
 * so. A foreign copy is the thing to look for, so name it.
 *
 * `own` is a LIST, not a boolean: two copies of the INSTALLED bundle can run at
 * once (launchd's KeepAlive service plus a LaunchServices/`open` launch of the
 * same .app), which is the duplicate the user actually sees — two agents marks
 * in the menu bar. Collapsing them to `running: true` reported that state as
 * healthy. The helper now refuses to be the second (SingleInstance.swift), and
 * `agents menubar setup` ends any duplicate a pre-fix helper left behind.
 *
 * Identity comes from `comm` (the resolved executable), never from a substring
 * of the command line: matching the latter flags any shell that merely mentions
 * the helper's name. `command` is consulted only to drop `--notify` one-shots.
 */
export declare function classifyMenubarProcesses(commOutput: string, commandOutput: string, installedExec: string): {
    own: MenubarProcess[];
    foreign: MenubarProcess[];
};
export interface MenubarStatus {
    platform: string;
    source: string | null;
    installedApp: string | null;
    /** The installed HELPER's version — not the CLI's. `local` for a dev build. */
    installedVersion: string | null;
    /** The helper version this install would put on disk right now. */
    currentVersion: string;
    /** The CLI's own version, reported separately so the two are never conflated. */
    cliVersion: string;
    stale: boolean;
    serviceInstalled: boolean;
    running: boolean;
    /** Live processes of the INSTALLED bundle. More than one is the duplicate. */
    instances: MenubarProcess[];
    /** Live MenubarHelper processes that are NOT the installed bundle. */
    foreignInstances: MenubarProcess[];
    disabledByUser: boolean;
}
export declare function getMenubarStatus(): MenubarStatus;
/** Read-only diagnostic for `agents menubar doctor` — probes, never mutates. */
export interface MenubarDoctorReport {
    platform: string;
    installPath: string | null;
    /** The installed HELPER's version — not the CLI's. `local` for a dev build. */
    installedVersion: string | null;
    /** The helper version this install would put on disk right now. */
    currentVersion: string;
    /** The CLI's own version, reported separately so the two are never conflated. */
    cliVersion: string;
    /** installed helper vs available helper. Never compares the CLI's version. */
    versionMatches: boolean;
    /** `unknown` on non-darwin or when nothing is installed to inspect. */
    signingIdentity: 'developer-id' | 'ad-hoc' | 'unknown';
    running: boolean;
    /** A live helper pid started before the installed bundle's on-disk mtime —
     *  it is running the binary that has since been swapped underneath it. */
    staleRunningProcess: PidStaleness[];
    /** Grant likely needs to be re-made: an ad-hoc identity breaks on every
     *  update, or a live process is confirmed to predate the current bundle. */
    accessibilityHintNeeded: boolean;
}
/** One live pid checked against the bundle's on-disk mtime. */
export interface PidStaleness {
    pid: number;
    stale: boolean;
}
/**
 * Pure comparison (no I/O) behind the stale-process check: a helper pid that
 * started before the bundle on disk was last written is still running the
 * PREVIOUS binary — the exact condition `restartMenubarHelperAfterSwap` exists
 * to fix. Millisecond epoch timestamps in, so the truth table is unit-testable
 * without a live process or filesystem.
 *
 * The two timestamps do not have the same resolution. `pidStartTimeMs` parses
 * `ps -o lstart`, which prints whole SECONDS, while the bundle mtime carries
 * sub-second precision — and the restart this check exists to detect happens
 * within a second of the swap that triggered it. A raw `<` therefore called a
 * healthy just-restarted helper stale on essentially every upgrade: measured on
 * zion at 1.22.46, pid start 1787441353000 vs bundle mtime 1787441353700, 700ms
 * apart inside one second, reported as "running the OLD binary" and surfaced as
 * `accessibilityHintNeeded` — telling the user to re-grant Accessibility after
 * an upgrade that had already restarted the helper correctly. So the bundle
 * mtime is truncated to the same whole second `ps` reports before comparing: a
 * pid that started in the swap's own second is fresh, and a genuinely stale pid
 * (a full second or more older) is still caught.
 */
export declare function isMenubarProcessStaleAgainstBundle(pidStartedAtMs: number, bundleMtimeMs: number): boolean;
/**
 * `agents menubar doctor` — everything a human needs to tell "why did
 * Accessibility ask again" apart from "the helper is genuinely broken":
 * install path, version skew, whether the signing identity is update-stable,
 * and whether a live process predates the bundle it's supposed to be running.
 * Read-only: no install, no restart, no TCC reset — `agents menubar setup`
 * remains the command that fixes what this reports.
 */
export declare function buildMenubarDoctorReport(): MenubarDoctorReport;
