import type { Logger, PtyHandle, Role, Channel } from './types.js';
import { type SystemctlRunner } from './systemd-scope.js';
import type { FsWatcher } from './fs-watcher.js';
import { type McpProbeResult, type McpProbeServerDescriptor } from './mcp-tools-probe.js';
import { type OwnerProfileBlock, type DormantPlugin, type ActivePlugin, type SpecialistDomain } from './system-prompt.js';
import type { HostResolution } from './config.js';
import type { ToolSurface } from './tool-surface.js';
import type { JsonlTail } from './jsonl-tail.js';
import type { PermissionModeTail } from './permission-mode-tail.js';
/** The per-spawn `permissions.deny` set for a public session (any channel):
 *  every CC native tool by bare name, a `(//**)` path form for every file tool,
 *  every harness tool by bare name, and the memory MCP server wildcard. Sorted
 *  and deduped so the emitted argv is stable and the audit test can compare. */
export declare function buildPublicDeny(): string[];
export interface PtyTracker {
    /** Intrinsic Claude session id. Map key, duplicated here so
     *  `forEachLive` callbacks don't need a closure over the key. */
    sessionId: string;
    /** "session_<suffix>" form from the PID file. Stable after the
     *  watcher's PID-file event resolves. */
    bridgeSessionId: string;
    /** Suffix-only convenience field (no "session_" prefix). */
    bridgeSuffix: string;
    pid: number;
    /** Live PtyHandle — the only place this reference lives. */
    pty: PtyHandle;
    /** Flipped true on PTY exit (natural or operator-requested). Read by
     *  /input (refuse 410) and the operator-request kill path (skip
     *  redundant destroy that would throw on a closed socket). */
    exited: boolean;
    /** Task 176 — true once `releasePtyMasterFd` has returned
     *  `master-fd=closed`. Defense in depth across the two exit branches
     *  (natural exit and operator request) — whichever loses the race
     *  skips the second destroy that would throw on real node-pty. */
    fdReleased: boolean;
    /** Hidden-spawn flag from /spawn body. Used by the LRU eviction
     *  hot path that does not want to round-trip through the sidecar. */
    hidden: boolean;
    /** Specialist agent name (or null for operator spawns). Same
     *  rationale as `hidden`. */
    specialist: string | null;
    /** Task 250 — systemd scope unit token for this spawn. The unit name
     *  is `claude-session-<scopeUnitToken>.scope`; `stopSession` and the
     *  reaper issue `systemctl --user stop` against that unit instead of
     *  `pty.kill`. Equals the intrinsic sessionId on `/resume` spawns
     *  (sessionId known pre-spawn) and a manager-minted UUID on fresh
     *  spawns. */
    scopeUnitToken: string;
}
/** Returns true when the manager has a live PtyHandle for `sessionId`.
 *  False for exited rows (the tracker is removed) and for sessions the
 *  manager never spawned (e.g. a JSONL-only archived row). */
export declare function isLive(sessionId: string): boolean;
/** Task 271 — read-only PID accessor for the live PTY at `sessionId`.
 *  Returns the PID when a tracker exists and has not exited, null
 *  otherwise. Used by the /promote refusal path so the 409 payload can
 *  identify the live process the operator must end first. Mirrors
 *  `isLive`'s encapsulation rule: returns a primitive, not the handle. */
export declare function livePidForSession(sessionId: string): number | null;
/** Task 455 — count of prior PTYs that exited for this sessionId within
 *  this manager-process lifetime. Read by the /stop route to surface a
 *  one-line observability hit when the count is > 0, so operators can
 *  see respawn history without changing the success-path log shape. */
export declare function priorExitedCountForSession(sessionId: string): number;
/** Test seam — read-only access to the tracker for a given session.
 *  Production routes never call this; they use the dedicated mutation
 *  exports (`writeInputToPty`, `killSession`, `onPtyExit`, etc.) so
 *  the PtyHandle stays encapsulated inside the module. */
export declare function getPtyTrackerForTests(sessionId: string): PtyTracker | undefined;
/** Register a callback to fire when the PTY for `sessionId` exits.
 *  Returns true when the tracker exists (callback was wired); false
 *  when no live PTY is tracked for that id (the caller's contract
 *  fires regardless — e.g. `attachPublicAudit` skips on no-tracker).
 *  Routes call this instead of reaching into the tracker for `.pty`. */
export declare function onPtyExit(sessionId: string, cb: () => void): boolean;
/** Iterate every live tracker. Used by index.ts's shutdown loop to
 *  SIGTERM each PTY synchronously, and by checkSlotsAndMaybeEvict's
 *  LRU search to walk specialists when the cap is hit. */
export declare function forEachLivePty(cb: (tracker: PtyTracker) => void): void;
/** Count of live trackers. Surfaced by /healthz so the operator can
 *  see process-side liveness without scanning the watcher's row index. */
export declare function livePtyCount(): number;
/** Test-only — clear the tracker map between vitest cases. Production
 *  never invokes this; the only legitimate eviction is PTY exit.
 *  Task 455 — also clears `priorExitedBySessionId` so tests that
 *  predate the exit-history counter (and only call this reset) do
 *  not leak counter state across vitest file boundaries. */
export declare function __resetPtyTrackerForTests(): void;
/** Test-only — clear the exit-history counter between vitest cases.
 *  Production never invokes this; the counter resets naturally on
 *  manager restart. `__resetPtyTrackerForTests` clears this map too;
 *  this narrower export is retained for tests that want to reset
 *  only the counter without touching the live tracker map. */
export declare function __resetExitHistoryForTests(): void;
/** Test-only — directly register a tracker without going through
 *  `spawnClaudeSession`. Used by FD-release / natural-exit tests that
 *  need to construct a known PtyHandle + tracker pair and exercise
 *  `killSession` / `handlePtyNaturalExit` against it. */
export declare function __registerPtyTrackerForTests(tracker: PtyTracker): void;
export type SpawnFailureReason = 'which-claude-not-found' | 'pty-spawn-failed' | 'pid-file-timeout' | 'pid-file-session-mismatch' | 'host-context-unresolved' | 'identity-unresolved' | 'soul-empty' | 'knowledge-missing' | 'knowledge-empty' | 'mcp-config-write-failed' | 'sidecar-write-failed' | 'dontask-empty-allowlist';
export type SpawnRejectionReason = 'specialist-cap-reached' | 'operator-slots-reserved' | 'low-memory';
export interface SpecialistCapRejection {
    kind: 'specialist-cap-reached';
    currentCount: number;
    cap: number;
}
export interface OperatorReserveRejection {
    kind: 'operator-slots-reserved';
    total: number;
    reserve: number;
    totalCap: number;
}
/** Task 600 — aggregate-RSS spawn gate. Returned when MemAvailable on the
 *  host drops below MIN_ADMISSION_MB, preventing a new specialist from
 *  driving the system into memory exhaustion. */
export interface LowMemoryRejection {
    kind: 'low-memory';
    memAvailableMb: number;
    thresholdMb: number;
}
/** Task 260 — what /spawn and /resume return to the caller. The
 *  metadata-rich `StoredSession` shape is gone; everything the wire
 *  payload needs (senderId, role, channel, url, startedAt, etc.) is
 *  read by the route from the sidecar (which `spawnClaudeSession`
 *  wrote before launching the PTY) and the watcher row. The success
 *  payload carries the in-process facts: the canonical sessionId
 *  (which may equal `args.resumeClaudeSessionId` on /resume), the
 *  pid, and the tracker reference for callers that need to attach
 *  exit-time handlers (`attachPublicAudit`,
 *  `attachSpecialistEndTurnAutoArchive`). */
export interface SpawnedSession {
    sessionId: string;
    pid: number;
    startedAt: string;
    /** URL captured by the time /spawn returns, or null when the
     *  `/remote-control` banner has not landed yet (the common case for
     *  fast spawns) or when the spawn is headless. The sidecar's `url`
     *  field is mutated when the banner does land; the route's SSE push
     *  re-emits the row from sidecar then. */
    url: string | null;
    tracker: PtyTracker;
}
export type SpawnResult = {
    ok: true;
    session: SpawnedSession;
} | {
    ok: false;
    reason: SpawnFailureReason;
    stderrTail: string;
} | {
    ok: false;
    rejected: SpecialistCapRejection | OperatorReserveRejection | LowMemoryRejection;
};
export interface SpawnDeps {
    /** Task 250 — spawn adapter takes a stable per-spawn unit token so the
     *  systemd-run --user --scope --unit=claude-session-<token>.scope can be
     *  composed at the adapter boundary. The adapter wraps node-pty around
     *  systemd-run; from the caller's perspective the contract is unchanged
     *  apart from the extra `unitToken` argument. */
    spawnPty: (cmd: string, args: string[], env: NodeJS.ProcessEnv, unitToken: string) => PtyHandle;
    spawnCwd: string;
    claudeBin: string;
    /** Max wait for the PID file to land after PTY spawn. Replaces the
     *  url-capture timeout as the readiness gate. */
    pidFileTimeoutMs: number;
    /** URL capture is best-effort and never blocks /spawn; this timeout is
     *  used purely for the parallel capture loop's bound. */
    urlCaptureTimeoutMs: number;
    logger: Logger;
    watcher: FsWatcher;
    host: HostResolution;
    accountDir: string;
    accountId: string;
    platformRoot: string;
    logDir: string;
    agentsDirs: Readonly<Record<Role, readonly string[]>>;
    perAccountSpecialistsDir: string;
    /** Per-brand Claude Code config dir — `$HOME/.<brand>/.claude`. The
     *  manager process inherits `CLAUDE_CONFIG_DIR` from its systemd unit;
     *  this field is the same value, plumbed through the deps surface so
     *  unit tests can substitute a tmp path. Claude Code resolves
     *  `--agent <name>` by reading `<claudeConfigDir>/agents/<name>.md`
     *  (Task 165). */
    claudeConfigDir: string;
    mcpConfigDir?: string;
    toolSurface: ToolSurface;
    /** Fires once after the watcher reports the PID-file CREATE event for
     *  the spawn AND the sidecar has been written to disk. http-server
     *  uses this to attach the public-tool audit watcher and the
     *  end-turn auto-archive subscriber. The callback receives the
     *  PtyTracker so callers can register `pty.onExit` handlers; route
     *  code reads spawn metadata from the sidecar (or via the watcher
     *  row that the sidecar populated). */
    onSessionReady?: (tracker: PtyTracker) => void;
    /** Task 189 — fires after the `/remote-control is active · <url>` banner
     *  is matched and the sidecar's `url` field has been written. http-server
     *  uses this to push a `row-updated` SSE so the client's hidden
     *  Open-in-new-tab arrow appears in step with the URL landing. Headless
     *  spawns never invoke this (the capture handler is not attached);
     *  url-capture-timeout never invokes this (no URL to push). */
    onUrlCaptured?: (sessionId: string) => void;
    tunnelUrlOverride?: string | null;
    /** Concurrent-specialist cap (per-account). See ManagerConfig. */
    specialistCap: number;
    /** Operator-slot reservation. See ManagerConfig. */
    operatorReserve: number;
    /** Total PTY ceiling (min of kernel pty/max and env). See ManagerConfig. */
    totalPtyCap: number;
    /** Task 600 — injectable for testing on non-Linux. Default implementation
     *  reads MemAvailable from /proc/meminfo and returns MB. Returns null when
     *  /proc/meminfo is absent (non-Linux dev machine). */
    readMemAvailableMb?: () => number | null;
    /** Task 173 — per-specialist set of tool names to strip from the
     *  `--allowed-tools` argv at spawn time. Sourced from the boot-time
     *  drift assertion's `brand-excluded-plugin` defects: every tool whose
     *  `mcp__<plugin>__*` namespace appears in the brand's
     *  `plugins.excluded` set lands in this map. A spawn whose specialist
     *  name has no entry is unaffected. Undefined means the manager booted
     *  before the strip-list plumbing landed; treat as empty. */
    specialistToolStripList?: ReadonlyMap<string, ReadonlySet<string>>;
    /** Task 207 — brand-scoped Neo4j endpoint, stamped on the PTY env block
     *  for specialist spawns so plugin-system MCP children inherit the
     *  connection without the per-spawn `.mcp.json` (dropped for specialists
     *  in favour of plugin manifests). Resolved at manager startup from
     *  `process.env.NEO4J_URI`; manager refuses to boot if absent (loud-fail
     *  replaces the silent regression where the recorder produced zero
     *  writes against a missing connection). Optional on the deps surface
     *  so existing test fixtures stay terse — tests that don't spawn a
     *  specialist never read this value; tests that do override it via the
     *  fixture's `deps()` factory. Production sites get the loud-fail at
     *  manager startup. */
    brandedNeo4jUri?: string;
    /** Task 207 — brand-scoped Neo4j password, source-of-truth read at
     *  manager startup from `process.env.NEO4J_PASSWORD` or from
     *  `<platformRoot>/config/.neo4j-password` (same file the UI and the
     *  memory plugin both read). Stamped on the PTY env so plugin-system
     *  MCP children inherit it without the per-spawn `.mcp.json`. Manager
     *  refuses to boot if neither source resolves. Optional on deps for the
     *  same reason as `brandedNeo4jUri`. */
    brandedNeo4jPassword?: string;
    /** Task 178 — shadow-probe each MCP server in the spawn's mcp.json to
     *  learn what each server offers, then emit [pty-spawn-tool-inventory].
     *  Production default delegates to `probeMcpServerTools` from
     *  `mcp-tools-probe.ts`; tests inject a fake. Per-server probe timeout
     *  defaults to 5 s — see `toolInventoryProbeTimeoutMs`. */
    readMcpToolInventory?: (req: {
        servers: ReadonlyMap<string, McpProbeServerDescriptor>;
        timeoutMs: number;
    }) => Promise<McpProbeResult>;
    /** Task 178 — per-server timeout for the tool-inventory probe. Default
     *  5_000 ms. On timeout the server's tool list is empty in the
     *  aggregated [pty-spawn-tool-inventory] line. */
    toolInventoryProbeTimeoutMs?: number;
    /** Task 250 — systemctl runner for scope-stop/list. Default uses
     *  spawnSync; tests inject a recording stub. Optional for older test
     *  fixtures that never reach the stop path. */
    runSystemctl?: SystemctlRunner;
    /** Task 183 — JSONL tail for the database-operator recorder spawn.
     *  pty-spawner calls `register(sessionId, { specialist, argvTools })`
     *  immediately after argv composition, BEFORE the PTY actually spawns,
     *  so the tail's state exists by the time the first JSONL modify event
     *  fires. Operator spawns pass through untouched — the tail filters by
     *  specialist on its caller side (only the database-operator spawn site
     *  registers). Optional so existing test fixtures stay terse. */
    jsonlTail?: JsonlTail;
    /** Task 278 — consumer of the per-session `<sessionId>.permission-mode.log`
     *  written by the binary's `--debug=auto-mode` capture. Optional so
     *  existing test fixtures stay terse. When present, http-server's
     *  jsonl-event subscriber calls `deriveFromFile` to compute the
     *  effective mode and update the sidecar on mismatch. */
    permissionModeTail?: PermissionModeTail;
}
/** Task 171 — specialists whose `--agent <name>` spawn has no human in the
 *  loop: no operator browser tab, no chat channel, no `claude.ai/code/session_<id>`
 *  URL to open. For these, `--remote-control` is dead weight (one outbound
 *  WebSocket to Anthropic per spawn plus a PTY-byte regex that never matches)
 *  and the URL-capture handler is not attached. Membership is by specialist
 *  name — the values that `SpawnArgs.specialist` carries — sourced from
 *  archive Task 085 (specialist plumbing) and this task. New headless
 *  specialists register their name here; channel-facing specialists
 *  (content-producer, librarian, personal-assistant, project-manager,
 *  research-assistant) stay out.
 */
export declare const HEADLESS_ROLES: ReadonlySet<string>;
export interface SpawnArgs {
    senderId: string;
    /** Task 205 — admin operator's userId (subject of the cookie session).
     *  Stamped onto the pty child env as `USER_ID` and into the per-spawn
     *  mcp.json placeholder map alongside `ACCOUNT_ID`. Absence is legal
     *  (silent-compaction MCP path, recorder specialist spawns); it lands
     *  as the empty string so MCP module init stays boot-tolerant per the
     *  Task 202 contract and the per-call refusal fires at first use. */
    userId?: string;
    role: Role;
    channel: Channel;
    resumeClaudeSessionId?: string;
    channels?: string[];
    permissionMode?: PermissionMode;
    attachmentDir?: string;
    aboutOwner?: OwnerProfileBlock;
    dormantPlugins?: readonly DormantPlugin[];
    activePlugins?: readonly ActivePlugin[];
    specialistDomains?: readonly SpecialistDomain[];
    hidden?: boolean;
    specialist?: string;
    /** Optional per-spawn model override. When set, the manager appends
     *  `--model <value>` to claude's argv. Sidebar spawns leave this
     *  unset; the recorder (database-operator) Stop hook sets `haiku`
     *  on its spawn body. Out of scope: a system-wide default. */
    model?: string;
    /** Optional initial user message. When set, the manager appends the
     *  string as the trailing positional argv to `claude`, which the CLI
     *  processes as the first user turn at session start. This makes the
     *  database-operator session's JSONL first `role=user` line equal the
     *  passed string verbatim — no bracketed-paste, no separate `/input`
     *  call. Task 153. */
    initialMessage?: string;
    /** Task 346 — public agent slug under `<accountDir>/agents/<slug>/`.
     *  Threaded onto the spawn env as the provenance / read-scope slug. */
    agentSlug?: string;
    /** Task 382 — `sessionId` UUID of the active `:AdminConversation` (or
     *  `:PublicConversation`), threaded onto both admin and specialist spawn
     *  envs as `SESSION_NODE_ID`. The memory/contacts MCP wrappers read
     *  this var and call `injectConversationProvenance`, which MATCHes
     *  `(c:Conversation {sessionId, accountId})` and prepends an inbound
     *  `:PRODUCED` edge to gated writes (`:Person`, `:Organization`, etc.).
     *
     *  Skipped when null/empty (autonomous spawns with no parent conversation —
     *  cron, scheduled-task — legitimately have no anchor and must thread an
     *  explicit `producedByTaskId` via `work-create` instead). */
    conversationNodeId?: string;
    /** Task 485 — per-visitor ringfence key for gated public agents. When
     *  present on a `role='public'` spawn, stamped on the child env as
     *  `ACCESS_SLICE_TOKEN`. The memory MCP server reads this and widens
     *  the `scope IN ['public']` read filter to
     *  `(n.sliceToken = $sliceToken OR n.scope = "public")`. Skipped on
     *  `role='admin'` and on open-mode public spawns. */
    sliceToken?: string;
    /** Task 485 — `elementId(:Person)` for the visitor whose grant owns
     *  the slice. Stamped as `ACCESS_PERSON_ID` so the session-end
     *  reviewer hook can pass it back when it dispatches the
     *  `public-session-reviewer` specialist. */
    personId?: string;
}
export type PermissionMode = 'default' | 'acceptEdits' | 'plan' | 'auto' | 'bypassPermissions' | 'dontAsk';
export declare const PERMISSION_MODES: readonly PermissionMode[];
interface McpConfigWriteParams {
    role: Role;
    toolSurface: ToolSurface;
    accountId: string;
    /** Task 205 — admin operator's userId. Threaded through to the
     *  placeholder map so `${USER_ID}` substitutes in `mcp.json` env
     *  blocks. Empty string when absent (silent-compaction / recorder
     *  paths). */
    userId?: string;
    platformRoot: string;
    logDir: string;
    /** Task 476 — sessionId minted by the spawner up front. Becomes both
     *  the `${SESSION_ID}` placeholder value substituted into the
     *  per-spawn mcp.json env blocks AND the disambiguating suffix on the
     *  mcp.json filename. One UUID end-to-end: also passed to claude via
     *  `--session-id` (fresh) or `--resume` (restored), used as the
     *  sidecar key, and returned to the caller. */
    sessionId: string;
    /** Task 476/487 — provenance slug substituted into `${AGENT_SLUG}`
     *  placeholders in PLUGIN.md env blocks. The admin operator gets the
     *  `'admin'` sentinel (so writes stamp `createdByAgent="admin"` instead
     *  of `"unknown"`); the specialist slug on subagent spawns; the public
     *  agent slug on public spawns. Every memory/work/contacts/scheduling/
     *  workflows/aeo plugin reads `process.env.AGENT_SLUG` purely as the
     *  `createdBy.agent` provenance stamp. Read-scope lives in AGENT_SLUG_SCOPE
     *  below — the two were one var until Task 487 split them. */
    agentSlug: string;
    /** Task 487 — read-scope slug substituted into `${AGENT_SLUG_SCOPE}`.
     *  Consumed ONLY by the memory plugin's memory-search to add
     *  `AND $agentSlug IN node.agents`. Empty string for the admin operator
     *  (search stays wide-open); the specialist slug on subagent spawns;
     *  the public agent slug on public spawns. This reproduces the pre-487
     *  AGENT_SLUG semantics exactly, so read-scope is unchanged for every
     *  role — only provenance (AGENT_SLUG) gained the 'admin' default. */
    agentScopeSlug: string;
    targetDir: string;
    logger: Logger;
    /** Task 165 — when the spawn is a specialist subagent, narrow the
     *  rendered mcp.json to only the MCP servers whose plugin namespace
     *  appears in the specialist's `tools:` frontmatter. `null` means the
     *  spawn is not a specialist; the role-based admin/public projection
     *  applies. */
    specialistTools: readonly string[] | null;
}
/** Task 165 — read the agent's `tools:` frontmatter line from
 *  `$CLAUDE_CONFIG_DIR/agents/<specialist>.md`. Loud-fails when the file
 *  is missing or when no `tools:` line exists in the frontmatter, since
 *  either case means `--agent <name>` would silently fall back to the
 *  admin surface — the exact defect Task 165 closes. Returns the tool
 *  names in the order they were declared (the caller sorts before
 *  emitting argv). */
export declare function readSpecialistTools(claudeConfigDir: string, specialist: string): string[];
export declare function expandPlaceholders(raw: string, vars: Readonly<Record<string, string>>): string;
/** Task 215 — pull the `<server>` segment out of a qualified MCP tool name
 *  the way Claude Code's plugin-tool registry surfaces it. Two shapes
 *  exist in the wild:
 *
 *   - Long prefix:  `mcp__plugin_<plugin>_<server>__<tool>` — emitted by
 *     Claude Code when an MCP server is registered via plugin manifest
 *     (post-Task-209 native plugin-system path; what `--agent <name>`
 *     specialist spawns receive in Registry #1).
 *   - Short prefix: `mcp__<server>__<tool>` — emitted by Claude Code when
 *     an MCP server is registered via `--mcp-config <path>` (admin operator
 *     spawns; what Registry #2 produces directly).
 *
 *  Both must resolve to the same `<server>` slug so the probe can match
 *  the qualified name against `toolSurface.mcpServers` keys, which the
 *  manager populates from PLUGIN.md basenames in `tool-surface.ts`. The
 *  pre-Task-215 inline regex `/^mcp__(.+?)__/` extracted `plugin_<plugin>_<server>`
 *  from the long-prefix shape (lazy match stops at the first `__`),
 *  which never matched any tool-surface key — the failure mode this
 *  helper closes.
 *
 *  Returns `null` for any name that matches neither shape. The caller
 *  surfaces those names through the inventory line's `unparseable-tools=`
 *  field so a malformed agent-frontmatter entry can't masquerade as a
 *  missing server. */
export declare function extractServerNameFromQualifiedTool(qualified: string): string | null;
/** Task 215 — classify the agent's frontmatter `tools:` list against the
 *  manager's tool-surface registry. Used by both the per-spawn `.mcp.json`
 *  writer (admin) and the tool-inventory probe (specialist). Returns the
 *  set of server names to spawn AND the two diagnostic sets the inventory
 *  line surfaces when non-empty: tokens that didn't parse at all
 *  (`unparseableTools`) and parseable tokens whose server isn't in the
 *  registry (`missingServers`). Both diagnostic sets are empty in the
 *  healthy case, so the existing log line shape stays unchanged for
 *  passing spawns. */
export declare function classifySpecialistTools(tools: readonly string[], toolSurfaceServers: ReadonlyMap<string, unknown>): {
    matchedServers: ReadonlySet<string>;
    unparseableTools: readonly string[];
    missingServers: readonly string[];
};
/** Builds the placeholder map for per-spawn `mcp.json` env substitution.
 *  Exported for Task 142 unit coverage that asserts `PLATFORM_PORT` and
 *  `MAXY_BRAND_PORT` resolve from the manager process env, and for
 *  Task 476 unit coverage that asserts SESSION_ID equals the passed
 *  sessionId and never the accountId. */
export declare function buildMcpPlaceholders(p: Pick<McpConfigWriteParams, 'platformRoot' | 'accountId' | 'userId' | 'sessionId' | 'agentSlug' | 'agentScopeSlug' | 'logDir'>, env?: NodeJS.ProcessEnv): Record<string, string>;
/** Test seam. Production never calls this; the `__` prefix matches the
 *  in-repo convention for non-API helpers. */
export declare function __setFirstTurnTimeoutMsForTests(ms: number): void;
export declare function __resetFirstTurnTimeoutMsForTests(): void;
/** Test seam — clear any leftover per-spawn state between tests so a
 *  failing test doesn't pollute the next one's Map. Production never
 *  invokes this; the only legitimate eviction path is PTY exit. */
export declare function __clearFinalSnapshotStateForTests(): void;
export declare function spawnClaudeSession(deps: SpawnDeps, args: SpawnArgs): Promise<SpawnResult>;
export interface StopDeps {
    killGraceMs: number;
    logger: Logger;
    /** Task 250 — systemctl runner for issuing the scope-unit stop. Default
     *  implementation invokes spawnSync; tests inject a recording stub. */
    runSystemctl?: SystemctlRunner;
    /** Task 427 — scope token from the resolved `SessionRow` when the row
     *  was reattached after a manager-process restart (Task 250 boot-seed).
     *  In that case `livePtys` is empty for the sessionId but the row
     *  carries the surviving scope unit token; passing it here lets
     *  `stopSession` issue `systemctl --user stop` against the scope even
     *  though the current manager never spawned that PTY. Null when the
     *  row has no scope token or when the caller has no row context. */
    rowScopeUnitToken?: string | null;
    /** Task 451 — claude pid from the resolved `SessionRow` when the row was
     *  reattached after a manager-process restart. Paired with
     *  `rowScopeUnitToken` so the row-scope stop branch can post-flight
     *  verify the kill — the bug this task fixes is exactly the case where
     *  `systemctl stop` returns 0 but the claude pid keeps running outside
     *  the scope cgroup. Null when the row has no pid (archived/ended) or
     *  when the caller has no row context. */
    rowPid?: number | null;
}
/** Task 250 — outcome of a stopSession call. The HTTP /stop route uses
 *  `path` + `scopeOutcome` to decide the HTTP status and response body.
 *
 *  Task 427 — `path` discriminates the three branches `stopSession` may
 *  take: `liveptys` (manager owns the PTY, original Task 250 path),
 *  `row-scope` (manager does not own the PTY but the row carries a
 *  surviving scope unit token from boot-seed reattach), `no-store-entry`
 *  (genuine miss — neither a tracker nor a row scope token). */
export interface StopSessionOutcome {
    existed: boolean;
    path: 'liveptys' | 'row-scope' | 'no-store-entry';
    /** Task 451 — `ok` now means unit stopped AND pid is gone (the pid was
     *  either reaped by the in-cgroup TimeoutStopSec or never had to be
     *  verified). `escalated-term`/`escalated-kill` mean the pid survived
     *  `systemctl stop` and the manager escalated via SIGTERM/SIGKILL
     *  outside the cgroup. `failed-pid-survives` is a distinct error class:
     *  the pid resisted SIGKILL and the operator-visible action did not
     *  achieve termination. */
    scopeOutcome: 'ok' | 'already-gone' | 'escalated-term' | 'escalated-kill' | 'failed-pid-survives' | 'error' | 'no-store-entry';
    exitCode: number | null;
}
/** Task 176 — natural-exit handler. Fires when the claude child exits on
 *  its own (operator `/quit`, SIGINT in the PTY, crash, network drop on
 *  `--remote-control`), with no operator `/stop` and no auto-archive in
 *  flight. Mirrors `killSession`'s post-Task-170 fd-release + tracker
 *  removal contract on the second exit branch the original task
 *  explicitly out-of-scoped. Without this, the master fd lingers in
 *  node-pty's internal socket until V8 GC finalises the IPty object,
 *  and the tracker entry survives until manager shutdown — so the
 *  shutdown loop SIGTERMs PIDs that already logged `process-exited`.
 *
 *  Side-effect order is load-bearing: set `exited` first so any
 *  concurrent `killSession` sees the already-exited branch; release the
 *  fd second (idempotent on a destroyed socket via the try/catch in
 *  `releasePtyMasterFd`); drop the tracker third (Map.delete on a
 *  missing key is a no-op, so the operator-request follow-up that
 *  races us is harmless). The log line carries the same `master-fd=...`
 *  suffix the operator-request path emits — every `kill … pid=<n>`
 *  line in `server.log` now ends in `master-fd=...`, no exceptions.
 *
 *  Exported so `__tests__/stop-session-fd-release.test.ts` can verify
 *  the contract end-to-end without re-implementing the handler in test
 *  code. */
export declare function handlePtyNaturalExit(deps: {
    logger: Logger;
}, tracker: PtyTracker, exit: {
    exitCode: number;
    signal?: number;
}, stderrTail: string): void;
/** Task 250 + 260 — operator-request stop. Issues `systemctl --user
 *  stop --no-block <scope>` against the PTY's scope unit (Task 250)
 *  instead of `pty.kill('SIGTERM')`; the scope's TimeoutStopSec=5 handles
 *  SIGTERM→SIGKILL escalation inside its own cgroup. Idempotent: a
 *  no-such-unit response (exit code 5) is treated as success because
 *  claude exited naturally and systemd retired the scope. The master fd
 *  is still released locally — fd ownership stays with the manager
 *  process and must be cleaned up there, irrespective of which process
 *  killed claude.
 *
 *  Task 260 — looks up the tracker in the module-scoped `livePtys` map
 *  (the metadata-rich SessionStore is gone). Deletes by identity (only
 *  if `livePtys.get(sessionId) === t`) so an LRU recycle or a
 *  cross-fixture sessionId collision in tests never clobbers a live
 *  PTY whose tracker was registered after the current call resolved. */
export declare function stopSession(deps: StopDeps, sessionId: string): Promise<StopSessionOutcome>;
/** Task 260 — alias preserved during the http-server callsite rename
 *  in this branch. The canonical export name is `stopSession` (Task 250). */
export declare const killSession: typeof stopSession;
/** Task 557 — open file-descriptor count for the manager process. Reads
 *  `/proc/self/fd` directly. Returns -1 on platforms that don't expose
 *  procfs (darwin dev Mac) or on read failure. Used by `[fd-census]`
 *  log lines so an rc-spawn leak is visible as `openFds` rising while
 *  `livePtys` stays flat across successive spawns. */
export declare function openFdCount(): number;
/** Task 558 — independent post-archive fd sweep. Runs on a timer (see
 *  `index.ts`), decoupled from spawn/archive request traffic so a leak
 *  surfaces within one sweep interval even with zero user activity.
 *
 *  Inputs are functions, not data, because the watcher's row state is
 *  owned by `index.ts`; this module owns `livePtys`. The sweep is the
 *  bidirectional reconciliation between them:
 *
 *    - For every entry in `livePtys` where `isArchived(sessionId)` →
 *      `archivedWithMaster` (the fd leak). Emits `op=orphan-master` and,
 *      if held longer than `ageEscalateMs` past tracker registration,
 *      `op=orphan-master-escalate`.
 *    - For every live (un-archived) session that the watcher knows
 *      about where `!livePtys.has(sessionId)` →
 *      `orphanLiveSessionsNoMaster` (the inverse defect; a live session
 *      that cannot operate because its master is gone).
 *
 *  Emits `op=post-archive-sweep` and `op=master-reconcile` once per
 *  invocation regardless of findings — the "zero leaks" steady state is
 *  itself a signal the sweep ran.
 *
 *  `archivedAtMs(sessionId)` is consulted per orphan-master; if the
 *  watcher cannot supply an archive timestamp, the sweep falls back to
 *  `trackerRegisteredAt` (the master cannot have been held longer than
 *  the manager has known about its tracker). `heldSinceArchiveMs` is
 *  the gap from that timestamp to wall-clock now.
 *
 *  The per-orphan `fd=` field is `unknown` today: node-pty's IPty does
 *  not expose its master fd number through the typed surface this
 *  manager imports, and walking `/proc/<pid>/fd/*` lsof-style is out of
 *  scope. The orphan's identity is the sessionId + pid; the missing fd
 *  number does not change the alarm semantics. */
export interface PostArchiveSweepDeps {
    logger: Logger;
    isArchived(sessionId: string): boolean;
    liveSessionIds(): string[];
    archivedAtMs(sessionId: string): number | null;
    ageEscalateMs: number;
    now?(): number;
}
export interface PostArchiveSweepSummary {
    archivedSessions: number;
    orphanMasters: number;
    liveTrackers: number;
    liveSessions: number;
    archivedWithMaster: number;
    orphanLiveSessionsNoMaster: number;
    escalated: number;
}
export declare function runPostArchiveSweep(deps: PostArchiveSweepDeps): PostArchiveSweepSummary;
/** Task 557 — register an rc-spawn PTY in the live tracker map so the
 *  master fd has a defined owner and release path. Mirrors the tracker
 *  shape `spawnClaudeSession` writes (Task 260), but skips the spawn
 *  pipeline's sidecar/watcher/url-capture concerns — `/rc-spawn` is
 *  fire-and-forget; claude registers itself with claude.ai/code as a
 *  Remote Control entry without manager-side bookkeeping. The `pty.onExit`
 *  hook lands on `handlePtyNaturalExit`, which marks the tracker exited,
 *  releases the fd, and deletes the entry — closing the leak gap the
 *  Task 552 non-PTY scope path created when it stopped registering a
 *  tracker at all. */
export declare function registerRcSpawnPty(deps: {
    logger: Logger;
}, sessionId: string, pty: PtyHandle, unitToken: string): PtyTracker;
/** Task 558 — the rc-spawn `op=fd-release trigger=session-ready` path
 *  (Task 557) is gone: the master IS the live session, not a reclaimable
 *  spawn artifact. The tracker is now released only by `handlePtyNaturalExit`
 *  (the slave exited on its own) or by `stopSession`'s `op=archive-release`
 *  (operator teardown). This export remains only because tests under
 *  `__tests__/` reach for it directly; its body is the same idempotent
 *  release-and-detach, called solely from those tests. Once those tests
 *  switch to the `op=archive-release` shape (Task 558 verification), this
 *  export can go. */
export declare function releaseAndUnregisterRcSpawnPty(deps: {
    logger: Logger;
}, tracker: PtyTracker, trigger: string): {
    suffix: string;
    openFds: number;
};
/** Task 260 — operator `/input` route entry. Writes to the tracker's
 *  PtyHandle via Claude TUI bracketed-paste semantics (Task 122). Returns
 *  a structured outcome so the route can choose the right HTTP status
 *  without reaching into the tracker. */
export interface WriteInputResult {
    ok: boolean;
    bytes?: number;
    error?: 'not-found' | 'exited' | 'write-failed';
    detail?: string;
}
export declare function writeInputToPty(sessionId: string, text: string, logger: Logger, channel: Channel | null): WriteInputResult;
export {};
//# sourceMappingURL=pty-spawner.d.ts.map