// Launch — the single session-bring-up primitive and the per-runtime adapter // contract. launch.launch is runtime-AGNOSTIC (env + argv + tmux new-session + // pipe-pane + self-TTL + boot + ready-gate + first-message delivery); the // runtime specifics (argv flags, system-prompt mechanism, boot dialogs, ready // markers, activity proxy, permission dialogs, resume) live behind RuntimeAdapter. // // Ownership split (blueprint §1, §7): launch = HOW to bring up ONE session; // lifecycle = WHEN / HOW MANY (wake/lock/reap/supervise). lifecycle.wakeOrSpawn // calls launch.launch; launch never decides whether or when to wake. The launch // path carries NO currency (no marketplace/plugin update) — that is install-time // (blueprint §0.6 fast-wake). // // This interface is FROZEN here (single author) so the per-runtime adapters can // be implemented independently against a known contract. import type { Intelligence, Runtime } from '../core/constants.ts' import type { ApprovalMode } from '../identity/index.ts' import type { PublicPeerSummary } from '../registry/index.ts' // Re-export so the adapters (which already import their contract types from here) get // ApprovalMode from ONE place rather than reaching into identity directly. export type { ApprovalMode } export type { PublicPeerSummary } from '../registry/index.ts' // ───────────────────────────────────────────────────────────────────────────── // composeSystemPrompt — the layered Канал-A merge (docs/Сборка системного // промпта — слои и каналы.md). Five layers, general → specific (local overrides // global): // 1. System YAML (identity + host facts) — jq-GOLDEN, byte-for-byte. // 2. iapeer doctrine: ~/.iapeer/IAPEER.md (global) + /.iapeer/IAPEER.md (local). // 3. Normalized peer registry (publicPeerSummary, exactly 5 fields). // 4. Plugin user-settings: every OTHER .md at the .iapeer/ root, global // + local merged per domain. Custom files (SPAWNER_INSTRUCTIONS.md, …) flow // in organically here — no special-casing. // 5. Doctrine fragments: every `*.md` in the `.iapeer/fragments/` subdir, global // (~/.iapeer/fragments/) + local (/.iapeer/fragments/) merged per stem. // PRIMITIVE-owned, machine-regenerated (e.g. a memory provider's guide + note index, …) // — a dedicated namespace kept OUT of the hand-authored IAPEER.md doctrine so // the auto-writer and the human writer never share a file. Sits LAST: the most // volatile layer, so edits to it never disturb the stable prefix above. // composeSystemPrompt is a PURE renderer over already-gathered data; the FS // discovery lives in gatherPromptInput (mirrors the bash split: shell read the // files, the renderer just laid out bytes). // ───────────────────────────────────────────────────────────────────────────── /** One merged block: the global + local halves of a single stem (either may be * absent), used for BOTH Layer 4 (`.md` at the .iapeer/ root) and Layer 5 * (`.md` in .iapeer/fragments/). `domain` is the filename stem, used only * for stable ordering — it is NOT emitted. Within a block global precedes local * (general → specific), and each half is rendered as its OWN per-file section * prefixed by a path marker (`globalPath` / `localPath`) — the assembly is now * per-file, not an organic stem-merge. */ export interface PromptDomainBlock { domain: string /** Global half content (general): ~/.iapeer/.md or ~/.iapeer/fragments/.md. */ global?: string /** Local half content (specific — overrides global): /.iapeer/… counterpart. */ local?: string /** Display path for the global half's marker (HOME abbreviated to ~). Absent → * no marker (pure-renderer callers). */ globalPath?: string /** Display path for the local half's marker. */ localPath?: string } export interface ComposePromptInput { personality: string description: string cwd: string /** System facts (claude-start.sh:228-247). */ platform: string osVersion: string user: string hostname: string today: string /** Layer 2 local: /.iapeer/IAPEER.md content. '' when absent. */ peerDoctrine: string /** Display path for the local IAPEER.md marker (HOME abbreviated to ~). */ peerDoctrinePath?: string /** Layer 2 global, OPTIONAL: ~/.iapeer/IAPEER.md content (sits between the YAML * block and the per-peer doctrine so per-peer overrides global). Existence- * gated: a present-but-empty file → '', an absent file → undefined. */ globalDoctrine?: string /** Display path for the global IAPEER.md marker. */ globalDoctrinePath?: string /** Layer 3: the normalized peer registry. Empty/omitted → the layer emits * nothing (and the output stays byte-identical to the legacy YAML+doctrine). */ peers?: PublicPeerSummary[] /** Layer 4: every non-IAPEER `.md` pair at the .iapeer/ root. Empty/ * omitted → the layer emits nothing. */ pluginDomains?: PromptDomainBlock[] /** Layer 5: every `.md` pair in the .iapeer/fragments/ subdir (global + * local), primitive-owned and machine-regenerated. Empty/omitted → the layer * emits nothing. Appended LAST (after Layer 4). */ promptFragments?: PromptDomainBlock[] } /** * Compose the merged system prompt, byte-for-byte equivalent to the claude-start * jq pipeline: * * ---\n * personality: \n * description: \n * peer-cwd: \n * platform: \n * os_version: \n * user: \n * hostname: \n * today: \n * ---\n * \n * [globalDoctrine + "\n" — only when present] * peerDoctrine * [\n\n + registry section — only when peers.length > 0] * [\n\n + merged domains — only when pluginDomains is non-empty] * [\n\n + merged fragments — only when promptFragments is non-empty] * * Layers 1+2 are byte-for-byte the legacy jq output; layers 3+4+5 are appended * (each as a `\n\n`-separated section) ONLY when they have content, so a peer * with no registry, no extra domains and no fragments produces the exact legacy * bytes. * * Each YAML value is a JSON string literal (jq @json: JSON.stringify), which is * also a valid YAML double-quoted scalar — safe against colons/quotes/newlines. * The keys use hyphen `peer-cwd` and underscore `os_version` exactly as the bash. */ export type ComposeSystemPrompt = (input: ComposePromptInput) => string // ───────────────────────────────────────────────────────────────────────────── // Launch spec + adapter config // ───────────────────────────────────────────────────────────────────────────── export interface LaunchAdapterConfig { claudeBin: string codexBin: string /** telegram-runtime launch binary (router runtime). */ telegramBin?: string /** notifier-runtime launch binary (router runtime, infra/always-on). */ notifierBin?: string /** voicetalk-runtime launch binary (router runtime, infra/always-on — the owner's voice channel). */ voicetalkBin?: string /** web-runtime launch binary (router runtime, infra/always-on — the owner's browser fleet-console). */ webBin?: string /** OpenCode native TUI binary (agent runtime, docs/20). Absent → `opencode` on PATH. */ opencodeBin?: string } export interface LaunchSpec { personality: string runtime: Runtime cwd: string /** `-` — the tmux session name + socket stem. */ identity: string /** Socket path (`/tmp/tmux-iap-.sock`). */ socketPath: string /** Composed system-prompt file path (tui runtimes that usesDoctrine). */ systemPromptFile?: string /** Resume the newest transcript/session for this cwd (adapter validates). */ resume?: boolean /** Pre-resolved resume ref (claude `--resume `); set by resolveResume. */ resumeRef?: string /** Internal capability reload: resume this exact proven-idle dialogue, not latest-for-cwd. */ exactResume?: boolean /** Free-form extra CLI args (PEER_START_ARGS). */ extraArgs?: string[] /** claude `--disallowedTools` value (opt-in per-peer override via launch.env `PEER_DISALLOWED_TOOLS`). * UNDEFINED (the default for the whole fleet) → the adapter's hardcoded default (`AskUserQuestion`); * an EXPLICIT empty string → the flag is OMITTED (allow all — e.g. re-enable AskUserQuestion); a * non-empty value → that verbatim list. Only claude's buildArgv consumes it (codex has no such flag). */ disallowedTools?: string /** Peer intelligence (artificial/natural/absent). Used to enforce an adapter's * intelligence gate at launch (telegram allows natural|absent). Optional: a doctrine- * less throwaway may omit it, but an adapter that declares allowedIntelligences * then refuses (cannot confirm the nature is in the set). */ intelligence?: Intelligence /** Human-approval mode (docs/17): `yolo` (default/omitted) launches with the * runtime bypass flag (current fleet behavior); `gated` launches WITHOUT bypass * and swaps in the gated permission/approval config, so the runtime's blocking * requests route to a human via the daemon broker (a PreToolUse hook, installed * separately at toggle time, is the interceptor). buildArgv reads it. */ approvalMode?: ApprovalMode /** Extra sandbox-writable roots BEYOND the peer cwd, for a gated codex peer launched under * `sandbox_mode=workspace-write` (docs/17). Consumed ONLY by codex's gated buildArgv (emitted as * `-c sandbox_workspace_write.writable_roots=[…]`) — claude/router/yolo ignore it, the same * codex-only shape as `disallowedTools` is claude-only. Resolved at spec-build from the host's * memory provider (the shared vault lives OUTSIDE every peer cwd, so a memory write would otherwise * hit codex's native modal); empty/omitted → no extra roots. */ writableRoots?: string[] } // ───────────────────────────────────────────────────────────────────────────── // Delivery markers — the tui submit surface (owned by the adapter) // ───────────────────────────────────────────────────────────────────────────── /** * Markers the transport submit path (submitIntoTui) needs to detect that a * bracketed paste landed in the input row before pressing Enter. Contract refactor * (docs/Рантайм-адаптеры): the ADAPTER owns delivery markers — they moved out * of the transport's hardcoded `PROMPT_GLYPHS = ['❯','›']` union so the generic * submit logic carries NO runtime strings and a new runtime ships its own glyph * with its adapter, not by editing transport. */ export interface DeliveryMarkers { /** Glyph(s) at column 0 of the rendered input-prompt row (claude '❯', codex '›'). * submitIntoTui locates the prompt row by these. A router has no submit surface * → empty array. */ promptGlyphs: string[] /** Extra "bracketed paste landed" indicators beyond the envelope's own tail-marker * (claude '[Pasted text' / '[Image #'). Optional — when absent the tail-marker is * the sole landed-signal. */ pastePatterns?: RegExp[] /** SGR parameter sequences that the runtime uses for composer ghost/placeholder * text (non-human input). The busy-composer delivery queue captures the pane * with ANSI escapes and treats visible text after the prompt glyph as HUMAN only * when at least one non-whitespace character is NOT under these dim/grey SGR * attributes. Runtime-owned for the same reason as promptGlyphs: color is TUI * surface, not transport policy. Examples observed on live panes: * `2` (faint/dim) and `38;5;246` (xterm 256-color grey). */ ghostTextSgr?: string[] } // ───────────────────────────────────────────────────────────────────────────── // Control commands (Ф-E, docs/Control-команды). The SECOND daemon channel: an // in-session control command (interrupt / compact) is mapped by the target's adapter // to a tmux send-keys sequence and performed UNCONDITIONALLY (immediate, NOT gated on // ready — the point is to interrupt / drive, not to wait). System commands (list / // status) are the daemon's own and do not reach the adapter. // ───────────────────────────────────────────────────────────────────────────── /** An abstract in-session control command (`interrupt`, `compact`, runtime-specific). */ export interface ControlCommand { name: string args?: readonly string[] } /** The runtime mechanism for a control command: a sequence of `tmux send-keys` * arg-lists, performed IN ORDER on the target session. */ export interface ControlPlan { /** Each inner array is one `tmux send-keys -t ` call's trailing args * (e.g. ['Escape'] or ['-l', '/compact'] then ['Enter']). */ sequence: string[][] /** Pause (ms) between sequence steps — e.g. typed text must settle before Enter. */ stepDelayMs?: number } // ───────────────────────────────────────────────────────────────────────────── // RuntimeAdapter — per-runtime "HOW to launch / observe one session" // ───────────────────────────────────────────────────────────────────────────── export interface RuntimeAdapter { runtime: Runtime /** 'tui' (claude/codex — pane boot/ready/dialogs) | 'router' (telegram — no TUI phases). */ kind: 'tui' | 'router' /** Does this runtime consume a composed system-prompt doctrine? (tui yes, router no). */ usesDoctrine: boolean /** * Delivery markers for the transport submit path (submitIntoTui) — the input- * prompt glyph(s) and optional "paste landed" patterns. The adapter OWNS them: * transport reads them from here instead of a hardcoded glyph * union. A router declares `{ promptGlyphs: [] }` (no submit surface). */ deliveryMarkers: DeliveryMarkers /** * How a delivered envelope's LANDING is confirmed AFTER the socket-ack, in * deliverViaHost (transport). Owned by the adapter because it is a property of * HOW the runtime logs an accepted message, not transport policy. A router * (kind:'router') is always socket-ack by structure and short-circuits before * this field is read; it refines the TUI runtimes. Absent → 'transcript' (the * strict default: a new/unknown runtime gets the swallow-guarding confirm, never * a weaker one). * - 'transcript' (claude): the runtime writes an acceptance record CARRYING the * envelope PROMPTLY — sub-second: a queue-operation when busy, the user-turn * when idle. So a message-specific transcript-carries-envelope check within a * short grace BOTH proves landing AND catches a paste swallowed at a turn * boundary (the false-OK class, incident 2026-06-23). Cheap and safe → keep it. * - 'socket-ack' (codex): the runtime writes NO acceptance record — its * session-jsonl user-input appears only when the model TURN INGESTS the * message, which during a long turn is many tens of seconds away (measured ~80s * live, 2026-06-25). But its input queue is DURABLE: a mid-turn submit is HELD * and processed at the next turn boundary, never lost (verified live — an 80s * turn still ingested + replied with the exact probe token). So the flushed * socket-ack (bytes left us AND drained to the pty) IS the delivery confirm; a * transcript grace here only ever FALSE-FAILs a message that WILL be processed, * wrongly escalating to a fallback peer. A genuinely dead session still fails * at the socket-ack (no socket / stalled flush) → really-dead still escalates. * - 'structural' (opencode): the runtime has NO pty submit surface at all — delivery goes through * the adapter's own `deliver` hook (a native control channel) whose acknowledgement already means * THIS message was persisted natively. deliverViaHost calls `deliver` instead of the pty paste. */ deliveryConfirm?: 'transcript' | 'socket-ack' | 'structural' /** * If set, launch REFUSES unless the peer's intelligence is IN this set (fail-loud — * also when the nature is unknown). A channel runtime carries a HUMAN (natural) OR a * FACELESS SERVICE bot (absent) but never an LLM agent (artificial): telegram/voicetalk → * ['natural','absent'] (sourced from allowedIntelligencesForRuntime — the single Ф0 truth). * Most adapters omit it (no nature gate — claude/codex/notifier). Source of intelligence → * docs/Идентичность; enforced by the launch primitive against LaunchSpec.intelligence. */ allowedIntelligences?: readonly Intelligence[] /** * Build the runtime argv: binary + flags, wiring systemPromptFile per-runtime * - claude: `--dangerously-skip-permissions [--disallowedTools …] --system-prompt-file [extra]` * - codex: `[resume --last] --no-alt-screen -C -c model_instructions_file= --dangerously-bypass-approvals-and-sandbox [extra]` * - telegram: `telegram-runtime run …` (no doctrine). * NO currency on this path. */ buildArgv(spec: LaunchSpec, cfg: LaunchAdapterConfig): string[] /** * If a known startup dialog is visible in `pane`, return the tmux send-keys * args to clear it (e.g. ['Enter'] or ['Down','Enter']); else null. (tui only). */ bootDialogKeys(pane: string): string[] | null /** * MID-SESSION nag/upsell auto-dismiss (livability). Distinct from bootDialogKeys: * the boot-driver stops at ready, but claude/codex can pop a ONE-TIME interactive * upsell modal AFTER the session is live (e.g. "Try the new fullscreen-renderer?") * that BLOCKS the pty waiting for a keypress no headless peer answers — it froze the * live fleet until a human cleared it. A persistent supervisor watcher (daemon.ts) * runs THIS off the authoritative model for the whole session and writes the keys. * Return the send-keys args that dismiss with the VERIFIED-SAFE default (decline), * else null. Match the FULL modal signature so ordinary content mentioning the * feature can never trigger a stray keystroke into the composer. Optional — a runtime * with no known mid-session nags (codex/router) omits it. tui only. */ nagDismissKeys?(pane: string): string[] | null /** * MID-SESSION blocking CIRCUIT-BREAKER confirm (livability + safety). A sibling of nagDismissKeys * for a DIFFERENT class: an interactive confirm that even `--dangerously-skip-permissions` does NOT * bypass because it sits ABOVE the permission layer — the "Dangerous rm operation on working * directory or its ancestor / Do you want to proceed? 1.Yes 2.No" guard claude 2.1.x added (no env, * settings, or flag disables it). A headless peer has no human to press 1/2, so it HANGS forever * (live-observed on a verification session). Owner decision (Артур, 04.07): auto-press YES — peers * already run on bypass, so this only restores the pre-2.1.x status quo; a hang is real recurring * harm. Return `{keys, denyKeys, taxonomy, detail}` (the AFFIRMATIVE keys + the taxonomy-specific * DECLINE keys + a class tag + a one-line trace of WHAT was approved) so the supervisor can press * YES on a yolo peer AND leave a post-hoc log, OR route the confirm to the human broker on a gated * peer and press the decision (Unit 4 — allow→keys, deny→denyKeys). `denyKeys` differs by taxonomy * because the modal layouts differ (dangerous-rm is a 2-option "1.Yes/2.No" → `['2','Enter']`; * command-approval is a 3-option "1.Yes/2.Yes,…/3.No" → `['3','Enter']`) — the adapter owns that * layout knowledge, not the supervisor. Match the FULL signature + cursor position so ordinary * content mentioning the phrase never fires; else null. Optional (codex/router omit it). tui only. * Human-approval lands via the daemon broker (docs/17); the vault idea is "Human-approval * подтверждения действий пира через Telegram-кнопки". * * `alwaysHuman` (default false) marks a KNOWN class that must NEVER be auto-pressed on ANY peer — * it always routes to the human broker regardless of approval mode (the owner's org-policy rule: * "a barrier ABOVE us is never auto-answered"). yolo auto-Yes applies only to alwaysHuman=false * classes (dangerous-rm / command-approval — the "yolo presses what it KNOWS" set). */ blockingConfirm?(pane: string): { keys: string[]; denyKeys: string[]; taxonomy: string; detail: string; alwaysHuman?: boolean } | null /** * GENERIC blocking-modal detector (docs/17 — yolo-robustness / unknown-modal). The residual sibling * of blockingConfirm: a numbered-SELECT modal that halts the pty on a keypress but matches NONE of * the adapter's KNOWN signatures (a new modal Anthropic/OpenAI shipped that we did not foresee — the * class that hung a live yolo peer). Detected STRUCTURALLY, not by phrase: the bottom-most cursor-glyph * row (`❯`/`›`) is a numbered option (`N.`) AND the contiguous block ending there carries ≥2 numbered * options — i.e. a live select REPLACED the composer (the same idle→composer / modal→option invariant * the known matchers rest on, minus the phrase gate). The adapter returns null for its OWN known * signatures (checked first by the caller), so this fires ONLY for the unrecognized residue. * * Returns `{ content, option1 }`: `content` = the VERBATIM modal block (question + all option rows, * CR-stripped, capped) shown to the human; `option1` = the verbatim label of option 1 (what an ALLOW * will press — the supervisor injects ['1','Enter'] on allow, ['Escape'] to cancel on deny). The human * sees the exact options and what Allow selects, so the decision is INFORMED (owner's "new I must SEE * and confirm" — предметно). v1 is BINARY (option-1-or-cancel); a >2-way choice on an unknown modal is * a v2 extension (most approvals are binary). The timing STUCK-gate (daemon) composes on top — this * fires only on a genuinely frozen pane. Optional (router omits it). tui only. */ unknownBlockingModal?(pane: string): { content: string; option1: string } | null /** Is the input surface ready for the first message? (tui: ready marker present * AND startup dialogs gone). Router runtimes return true (no input surface). * `mode` (default yolo) lets a runtime whose READY marker differs by approval mode * pick the right one — claude's yolo ready banner ('bypass permissions on') is * absent under gated (launched WITHOUT --dangerously-skip-permissions). */ isInputReady(pane: string, mode?: ApprovalMode): boolean /** Newest activity-proxy FILE mtime — used by the READY-GATE (it waits for ANY transcript write past * baseline = "the model produced its first turn"). NOT for idle accounting: a live session re-saves * this file without a new entry, so it is falsely fresh at idle. null for a router (no proxy). */ newestActivityMtime(cwd: string, ctx?: ObserveContext): number | null /** Content-timestamp (epoch ms) of the LAST meaningful transcript ENTRY — the reliable IDLE-accounting * signal. Unlike newestActivityMtime (file mtime, re-touched at idle) and the pane-log mtime (a * statusline re-render ticks it at idle), the entry stream only advances on real turn activity. * superviseTick uses THIS for idle/quiet age. null for a router (no transcript). */ lastTurnMtime(cwd: string, ctx?: ObserveContext): number | null /** В58 — newest FILE mtime among the session's CHILD workflow/subagent transcripts (a running workflow * writes them continuously). A peer that launched a long workflow (>1h) makes NO turns of its own, so * lastTurnMtime is stale and the idle-reap would cut the busy session mid-work. superviseTick folds * THIS into the idle proxy so live child work keeps the session alive. Optional — null / undefined for * runtimes without a subagent layout (routers, codex). */ childActivityMtime?(cwd: string): number | null /** Resume preflight — validate a resume request fail-loud (never silent fresh). * Returns the resolved ref (claude uuid) or ok:false with a reason. */ resolveResume(cwd: string, ctx?: ObserveContext): { ok: boolean; ref?: string; reason?: string } /** * Map an abstract in-session control command to this runtime's mechanism (a tmux * send-keys sequence — ControlPlan), or null when the runtime does not support it * (the daemon/CLI surfaces an explicit refusal). Ф-E, docs/Control-команды: * - tui (claude/codex): `interrupt` → ['Escape'] (claude ×1; codex ×1-2, snapped * live), `compact` → type '/compact' then Enter. Declares the supported set. * - router (telegram/notifier): no TUI turn → null for everything (refuse). * Performed UNCONDITIONALLY (immediate, not ready-gated) — the point is to * interrupt / drive a possibly-stuck session, exactly when normal delivery wouldn't. */ executeControl(command: ControlCommand): ControlPlan | null // ── Structural runtime hooks (docs/20 — OpenCode). All optional; a pty-glyph runtime (claude/codex) // omits every one and launch/transport stay byte-identical for it. ───────────────────────────────── /** Pre-spawn preparation (writes runtime-owned launch artifacts, e.g. the core plugin) → extra child * env merged into the composed invocation BEFORE the identity ABI. `baseEnv` is the composed child env * (base + launch.env) so an operator inline config is merged, not discarded. `readyToken` identifies * THIS process launch for structuralReady (e.g. a launchID). Throws → launch FAILS loud. */ prepareLaunch?(spec: LaunchSpec, baseEnv: NodeJS.ProcessEnv, cfg: LaunchAdapterConfig & { env?: NodeJS.ProcessEnv }): PreparedLaunch /** Structural readiness replacing the pane-glyph gate: resolves when the runtime itself reported * readiness for `readyToken` (fail on deadline / reported error / `alive()` false). */ structuralReady?(spec: LaunchSpec, readyToken: string, deadlineMs: number, alive: () => boolean, env: NodeJS.ProcessEnv): Promise<{ ok: boolean; reason?: string; warnings?: string[] }> /** Structural delivery (deliveryConfirm 'structural'): resolves ok only when THIS message is accepted * and persisted by the runtime. */ deliver?(target: { personality: string; identity: string }, text: string, env: NodeJS.ProcessEnv): Promise<{ ok: boolean; error?: string; messageID?: string }> /** Structural in-session control (interrupt, …). A runtime that declares it is controlled through * THIS, not executeControl's pty key plan. Unsupported command → ok:false with an explicit reason. */ control?(target: { personality: string; identity: string }, command: ControlCommand, env: NodeJS.ProcessEnv): Promise<{ ok: boolean; error?: string }> } /** Optional per-peer observation context for adapters whose state is personality-scoped (opencode keeps * it under /state/opencode/

, not per-cwd). cwd-keyed runtimes ignore it. */ export interface ObserveContext { personality: string env?: NodeJS.ProcessEnv } export interface PreparedLaunch { env: Record readyToken?: string /** Non-fatal launch facts the primitive records in lifecycle.log (ev=launch-warning). */ warnings?: string[] } // ───────────────────────────────────────────────────────────────────────────── // launch primitive // ───────────────────────────────────────────────────────────────────────────── export interface LaunchConfig extends LaunchAdapterConfig { sockDir: string bootDeadlineSecs: number /** Seconds an unrecognized boot modal must stay unchanged before the bring-up fails fast. */ bootModalSecs?: number readyGateSecs: number /** Log dir for pipe-pane output. */ logDir: string /** * Durable EXIT-CAUSE log dir (~/.iapeer/logs/iapeer — next to lifecycle.log, * where the investigator looks). When set, launch installs a tmux `pane-died` * hook that records WHY the session's process died (exit status / signal) AT THE * MOMENT of death, into `/exits.log` — the blind spot the daemon's * 60 s supervise tick (reaped-gone) can only see post-factum, after the exit code * is already lost. Routed through cfg (NOT re-resolved from env) so it is isolated * by the same sandbox as the rest of launch; a FALSY dir → no hook installed and * `remain-on-exit` stays off (original behavior — a partial/test cfg never writes * and never lingers a dead pane). See exitCauseHook in index.ts. */ exitLogDir?: string env?: NodeJS.ProcessEnv /** * Always-on bring-up (infra runtimes held by launchd KeepAlive). Marks the * session as infra-owned: launchd (KeepAlive) owns its lifecycle, the daemon is * READ-ONLY for it (H4), and it opts out of the warm-on-demand `-v` tmux * command-log (a rare-relaunch infra session has no rotation point and is not the * dying warm class). Historically it also skipped the session self-TTL; that TTL * was removed in 0.2.55 (see launch step 4), so the exemption is now moot. */ alwaysOn?: boolean } export interface LaunchResult { status: 'READY' | 'FAILED' identity: string process_address: string reason?: string /** Set iff the bring-up failed FAST on an unrecognized boot modal (waitHostReady fail-fast). */ bootModal?: { content: string; option1: string } } /** * Bring up ONE session: pre-clean stale tmux server → tmux new-session -d with * adapter.buildArgv → pipe-pane → boot (answer dialogs via * adapter, wait for adapter.isInputReady, deliver the first message via * load-buffer + bracketed paste — the same byte-path as warm delivery) → * ready-gate (adapter.newestActivityMtime strictly advances). * Runtime-agnostic; all specifics come from the adapter. Returns READY/FAILED. * `firstMessage` (the task / routed envelope) is delivered as the boot message; * a router runtime skips the TUI boot/ready phases. */ export type LaunchFn = ( spec: LaunchSpec, adapter: RuntimeAdapter, firstMessage: string, cfg: LaunchConfig, ) => Promise