/** * Validate that a name contains only safe characters for tmux CLI usage. */ export declare function isValidTmuxName(name: string): boolean; /** * Sanitize a string to contain only safe tmux name characters. */ export declare function sanitizeTmuxName(name: string): string; /** * Generate a tmux session name for a parent pi session. * Format: pilot- */ export declare function makeSessionName(parentSessionId: string): string; /** * Legacy session name for a parent pi session (`_pi-sub-`), * used by records persisted before the `pilot-` rename. Never used to CREATE * sessions — only to recognize ownership of legacy records (reconciler adopt * policy, widget scoping) until old records drain. */ export declare function makeLegacySessionName(parentSessionId: string): string; /** * Generate a tmux window name for a subagent. * Format: -- */ export declare function makeWindowName(harnessName: string, agentType: string, agentId: string): string; /** * Check if tmux is available on PATH. */ export declare function isTmuxAvailable(): boolean; /** * Identity written to the sidecar manifest when a fresh isolated server is * bootstrapped. Provided only by the composition root's own instance — * instances constructed for GC/reconciliation never bootstrap servers. */ export interface TmuxSocketManifestInfo { projectPath: string; parentSessionId: string; } /** * Wrapper around tmux CLI commands via child_process.execFileSync. * * All commands use argv-based execution to prevent shell injection. * All commands throw on failure with a descriptive error message. * * An instance is SOCKET-SCOPED (design D2): when constructed with a * `socketPath`, every command runs against that dedicated server * (`tmux -S ...`); constructed without one, it addresses the * user's default server (legacy records, tests). The socket is instance * state, not a per-call parameter, so a forgotten argument can never * silently target the wrong server. Use `tmuxForSocket()` to construct * instances outside of tests. */ export declare class TmuxManager { readonly socketPath?: string | undefined; private readonly manifestInfo?; constructor(socketPath?: string | undefined, manifestInfo?: TmuxSocketManifestInfo | undefined); /** Prepend `-S ` when this instance is socket-scoped. */ private tmuxArgs; /** * Whether a tmux server is currently running on this instance's socket. * Only meaningful for socket-scoped instances (used to detect the * first-session bootstrap case). */ private serverIsRunning; /** * One-time setup after starting a fresh isolated server (design D7): * `renumber-windows off` globally and `exit-empty on` (server option — the * server self-terminates when its last session closes), then the sidecar * manifest with the server pid so GC/audits can enumerate and re-verify * our servers. */ private bootstrapFreshServer; /** * Ensure a detached tmux session exists for the given parent session. * If the session already exists, returns the existing session name. * Does NOT create a session — use createWindow for the first window * which creates the session in one step. * Returns the session name. */ ensureSession(parentSessionId: string): string; /** * Check if a tmux session exists. */ sessionExists(name: string): boolean; /** * Destroy a tmux session, killing all windows and processes. */ destroySession(name: string): void; /** * Fixed name for the per-parent park window. Keeps `pilot-` alive * after the last agent window is reaped (tmux otherwise auto-destroys a * session with no windows, which detaches any client watching it). */ static readonly IDLE_WINDOW_NAME = "_idle"; /** * Ensure a long-lived park window exists in `sessionName` so the session * survives agent-window teardown. No-op if the session is gone or the * idle window already exists. Detached (`-d`) so an attached client stays * on whatever window they were watching. */ ensureIdleWindow(sessionName: string): void; /** * Kill this instance's dedicated server outright. Best-effort — the * server may already have exited on its own (`exit-empty on`). Guarded to * socket-scoped instances so it can never kill the user's default server. */ killServer(): void; /** * Create a new window in a session. If the session does not yet exist, * creates the session with this as its first window via `new-session -d`. * If the session already exists, creates a new window via `new-window`. * Returns `{ windowId, windowIndex }`. * * The window's stable tmux ID (`#{window_id}`, e.g. `@5`) is captured * atomically via `-P -F` on the SAME command that creates the window — * there is no separate resolve-by-name step (and thus no race) between * creation and learning the ID. `windowIndex` is retained only to compose * the display-only `recoveryId`; every live operation after creation * targets by `windowId` (see the `*ById` methods below). * * When the session is newly created on an ISOLATED server, `renumber-windows * off` is set on it immediately (once per session, not per window) so * sibling-window teardown never shifts indices within our own sessions * regardless of the user's global tmux config. On the user's own server * (current-session topology) no option is set — stable-ID targeting * (ADR 0002) provides renumber-safety without touching their config. * * When this instance is socket-scoped and no server is running on its * socket yet, the `new-session` that starts the server passes * `-f /dev/null` (the user's tmux config is never read) and the fresh * server is bootstrapped with explicit options + the sidecar manifest * (see `bootstrapFreshServer`). * * To prevent a race condition where the command finishes before * remain-on-exit is set (causing the window to close), we: * 1. Create the window WITHOUT a command * 2. Set remain-on-exit * 3. Send the command as keyboard input via send-keys */ createWindow(sessionName: string, windowName: string, command: string): { windowId: string; windowIndex: number; panePid?: number; }; /** * Send keys (short message) to a tmux window using argv-based execution. * Uses `tmux send-keys -t -l ` via execFileSync to * prevent shell injection from user-controlled text. */ sendKeys(sessionName: string, windowIndex: number, text: string): void; /** Renumber-safe `sendKeys` — see the NAME-based variants note below. */ sendKeysByName(sessionName: string, windowName: string, text: string): void; private sendKeysToTarget; /** * Send literal text followed by Enter keypresses. * Uses sequential `execFileSync` calls with a brief delay between them * to ensure the TUI has processed each input before the next arrives. * Sends Enter twice as a safety net in case the first is consumed * during text input processing by the TUI. */ sendKeysWithEnter(sessionName: string, windowIndex: number, text: string): void; /** Renumber-safe `sendKeysWithEnter` — see the NAME-based variants note below. */ sendKeysWithEnterByName(sessionName: string, windowName: string, text: string): void; private sendKeysWithEnterToTarget; /** * Send keys (long/multiline message) using temp file + load-buffer + paste-buffer. * * The paste MUST use bracketed-paste mode (`-p`) and preserve line feeds * (`-r`). pi-tui (and cmd's TUI) enable bracketed paste (`\x1b[?2004h`) and * collect everything between the `\x1b[200~`/`\x1b[201~` markers as a SINGLE * literal input. Without `-p`, tmux does a plain paste and — by default — * rewrites each `\n` to `\r`, which the TUI reads as Enter, submitting a * multi-line prompt one line at a time (so e.g. each URL in a research prompt * arrives as its own separate prompt). `-r` additionally keeps newlines as * `\n` rather than `\r` inside the bracketed block. The caller sends a single * Enter afterwards (see spawner Phase 6) to submit the assembled input once. */ sendKeysLong(sessionName: string, windowIndex: number, text: string): void; /** Renumber-safe `sendKeysLong` — see the NAME-based variants note below. */ sendKeysLongByName(sessionName: string, windowName: string, text: string): void; private sendKeysLongToTarget; /** * Send a newline (Enter) to a tmux window. */ sendEnter(sessionName: string, windowIndex: number): void; /** Renumber-safe `sendEnter` — see the NAME-based variants note below. */ sendEnterByName(sessionName: string, windowName: string): void; private sendEnterToTarget; /** * Send Ctrl+C to a tmux window (kill the foreground process). */ sendCtrlC(sessionName: string, windowIndex: number): void; /** Renumber-safe `sendCtrlC` — see the NAME-based variants note below. */ sendCtrlCByName(sessionName: string, windowName: string): void; private sendCtrlCToTarget; /** * Send a tmux KEY NAME / combo (e.g. `Escape`, `C-x`) — NON-literal * `send-keys` (no `-l`), so tmux interprets the key name instead of typing * the text into the composer. Used by the stop-combo path for combos other * than `C-c` (codex's `Escape` interrupt). */ sendKeyComboByName(sessionName: string, windowName: string, combo: string): void; /** ID-based `sendKeyCombo` twin (stable `@N` target, renumber-proof). */ sendKeyComboById(windowId: string, combo: string): void; /** * Capture pane contents (for inspection). */ capturePane(sessionName: string, windowIndex: number, lines?: number): string; /** Renumber-safe `capturePane` — see the NAME-based variants note below. */ capturePaneByName(sessionName: string, windowName: string, lines?: number): string; private capturePaneFromTarget; /** * Check if a window exists in a session. * Returns true only when the target window index is found. */ windowExists(sessionName: string, windowIndex: number): boolean; /** * Kill a single window in a session. Best-effort — no throw if the window * or session is already gone. Killing the window terminates the process * (e.g. the pi/cmd TUI) running in it. */ killWindow(sessionName: string, windowIndex: number): void; /** * Kill a window by its NAME rather than its positional index. Best-effort. * * Prefer this for teardown: with `renumber-windows on` (common in user * configs), killing a window renumbers the rest, so a stored index goes * stale and index-based kills hit the wrong window — leaving siblings alive * in a parallel wave. Window names embed the agentId and never renumber. */ killWindowByName(sessionName: string, windowName: string): void; /** * Whether a window with the given NAME exists in the session. Renumber-safe * (unlike `windowExists`, which matches by index). */ windowExistsByName(sessionName: string, windowName: string): boolean; /** ID-based `sendKeys` — the correct default over `sendKeys`/`sendKeysByName`. */ sendKeysById(windowId: string, text: string): void; /** * Send literal text followed by Enter keypresses (see `sendKeysWithEnter` * for the rationale on the delay/double-Enter), targeted by stable ID. */ sendKeysWithEnterById(windowId: string, text: string): void; /** ID-based `sendKeysLong` (bracketed paste via temp file), see `sendKeysLong` for rationale. */ sendKeysLongById(windowId: string, text: string): void; /** ID-based `sendEnter`. */ sendEnterById(windowId: string): void; /** ID-based `sendCtrlC`. */ sendCtrlCById(windowId: string): void; /** ID-based `capturePane`. */ capturePaneById(windowId: string, lines?: number): string; /** ID-based `killWindow`. Best-effort — no throw if the window is already gone. */ killWindowById(windowId: string): void; /** * Whether a window with the given stable ID exists anywhere on this * server. IDs are server-global, so this lists across all sessions * (`-a`) rather than scoping to one. */ windowExistsById(windowId: string): boolean; /** * Look up the current `#{window_name}` for a stable window ID, across all * sessions on this server. Returns `undefined` if the ID is not found or * the lookup fails. Used by the ID+name liveness agreement check (D5): * a stored ID matching a window with a DIFFERENT name (e.g. after a tmux * server restart reassigns IDs) must be treated as the window being gone. */ windowNameById(windowId: string): string | undefined; /** * Count the windows currently in a session. * * Returns **-1** when the window count cannot be determined (the `tmux * list-windows` call errored). Callers MUST treat -1 as "unknown", NOT as * empty: misreading a transient error as 0 caused a premature * `destroySession` that killed the shared `pilot-` session out from * under sibling subagents still finishing (so their cleanup never ran). * * A genuinely empty result (session gone → tmux auto-destroys a session when * its last window is killed) also surfaces as an error here and returns -1; * that's fine, since there is then nothing to destroy. Likewise a server * that already exited (`exit-empty on`) surfaces as -1 — "server gone" is * never an error for teardown, just an unknown count with nothing left to * destroy. */ countWindows(sessionName: string): number; } export declare function tmuxForSocket(socketPath?: string, manifestInfo?: TmuxSocketManifestInfo): TmuxManager; /** Test hook: reset the per-run instance cache. */ export declare function clearTmuxForSocketCache(): void; /** * Resolve the `TmuxManager` that reaches a RECORD's server. Code holding the * parent's own instance but operating on persisted records (GC, reconciler, * steer/resume on recovered records) must not assume the record lives on the * parent's socket: a legacy record (no `socketPath`) lives on the default * server, and another parent's record lives on that parent's socket. When * the record's socket matches the given instance's, the instance is reused * (also keeps tests injecting a fake manager working — fake and record both * carry `socketPath: undefined`). */ export declare function tmuxForRecord(record: { socketPath?: string; }, tmux: TmuxManager): TmuxManager; /** * Build a shell command string with proper escaping for the given parts. * Shell-escapes arguments using single quotes with handling for embedded single quotes. */ export declare function shellEscape(args: string[]): string; //# sourceMappingURL=tmux-manager.d.ts.map