/** * Tmux isolation spawner — runs child agent in a new tmux window via wrapper script. * Spec: §4.4 — Tmux Isolation */ import { SessionManager } from "@earendil-works/pi-coding-agent"; import { execFile } from "child_process"; import { notifySession } from "@pi-orca/core"; import type { AgentLifecycle } from "@pi-orca/core"; import type { ResolvedAgentTemplate } from "../template.js"; import type { SpawnRespawnSpec } from "./sdk.js"; declare const execFileAsync: typeof execFile.__promisify__; export interface TmuxAgentRecord { /** * Effective target this agent was spawned into. Drives the `stopTmuxAgent` * choice between `kill-pane` and `kill-window`. */ tmuxTarget: "pane" | "window"; /** * Containing window id. Always populated for tmux agents (it's the window * the pane lives in when target=pane, or the window itself when target=window). */ tmuxWindowId: string; /** Pane id when target=pane; empty string when target=window. */ tmuxPaneId: string; scriptPath: string; promptTempDir: string; sessionPath: string; /** PID populated from the child's first heartbeat write. */ pid?: number; /** Lifecycle (spec §5.13). */ lifecycle: AgentLifecycle; /** * Absolute path to the per-child control socket (persistent agents only). * Empty string for one-shot tmux agents — the parent has no need to * push `agent-control: prompt` at them. Spec §5.13. */ childControlSockPath: string; /** Parent session path captured so the prompt helper can flip status. */ parentSessionPath: string; } export declare function getTmuxAgent(agentId: string): TmuxAgentRecord | undefined; export declare function listTmuxAgents(): ReadonlyMap; export declare function clearTmuxAgents(): void; export declare function setTmuxAgentPid(agentId: string, pid: number): void; /** * Probe for tmux availability via `tmux -V`. Cached module-scoped. * Spec: §4.4.1 round-2 W30 * * @param exec - Optional exec function override for testing * @returns True if tmux is available */ export declare function checkTmuxAvailable(exec?: typeof execFileAsync): Promise; /** * Reset the cached tmux availability check. * For tests; resets between runs. */ export declare function resetTmuxAvailability(): void; /** * Quote a string as a POSIX single-quoted shell argument. * Embedded `'` is escaped as `'\''`. * Spec: §4.4.1 shellQuoteSingle * * @param s - String to quote * @returns Single-quoted shell-safe string */ export declare function shellQuoteSingle(s: string): string; /** * Flatten a NodeJS env object into a sequence of `-e VAR=value` argv pairs for * `tmux new-window`. Each tmux argv entry is a separate string so shell quoting * is unnecessary — the values can contain spaces, `=`, `$`, quotes, etc. * * Entries with undefined values are skipped (matches Node's spawn semantics). * Names containing `=` or `\0` are skipped because tmux would mis-parse them * (the first `=` ends the name); these aren't valid POSIX env names anyway so * dropping them matches what a well-formed `process.env` already does. * * @param env - Environment object (typically `process.env`) * @returns Flat argv array, e.g. `["-e", "FOO=1", "-e", "BAR=2"]` */ export declare function buildTmuxEnvArgs(env: NodeJS.ProcessEnv): string[]; interface WrapperScriptParams { promptFile: string; sessionFile: string; exitJsonPath: string; flagList: string; piInvocation: string; } /** * Build the wrapper script body. * Spec: §4.4.1 — robust to SIGTERM/SIGQUIT/SIGINT/SIGHUP, writes exit.json fallback * for crashes before exec(). * * The prompt is delivered as a `@` argument, NOT via stdin redirection. * pi-coding-agent's `resolveAppMode` (main.js) forces print mode whenever * `process.stdin.isTTY === false`, and print mode exits after one prompt — * which would close the tmux window immediately and break the whole point of * tmux isolation (visible interactive turn in the pane). Leaving stdin * attached to the tmux pane TTY keeps pi in interactive mode; `@file` loads * the task text as the initial message (see `buildInitialMessage` in * pi-coding-agent/dist/cli/initial-message.js). */ export declare function buildWrapperScript(params: WrapperScriptParams): string; export interface SpawnTmuxParams { agentId: string; resolved: ResolvedAgentTemplate; task?: string; /** * Parent session's relative path (canonical form, portable across hosts). * Used for `.orca/agents/` layout and as the cross-machine fallback when * `parentSessionFile` doesn't exist locally. Spec §2.6.1. */ parentSessionPath: string; /** * Parent session's absolute file path on the host that wrote the status. * Preferred for `SessionManager.forkFrom` when it still exists locally; * otherwise we resolve `parentSessionPath` under the local sessions root. * Omit or pass "" when the parent has no on-disk file (in-memory * SDK sessions). Spec §2.6.1. */ parentSessionFile?: string; parentSessionId: string; rootSessionPath: string; parentNotifySockPath?: string; cwd?: string; /** Override Pi session directory (for SessionManager.create / forkFrom) */ sessionDir?: string; /** * Test-only override for tmux exec. * Defaults to `promisify(execFile)`. */ execOverride?: typeof execFileAsync; /** * Environment passed to the spawned tmux window via `tmux new-window -e VAR=value`. * Defaults to `process.env`. tmux's default behavior is to spawn the new window * with the *server's* environment (not the client's), which means a `pi` started * in a fresh shell with new API keys would NOT see those keys in spawned agents. * Forwarding explicitly via `-e` makes tmux children reach env parity with RPC * children (RpcClient does `{ ...process.env, ...env }` internally). * Test-only override. */ envOverride?: NodeJS.ProcessEnv; /** * Test-only override for SessionManager. */ sessionManagerFactory?: { create: (cwd: string, sessionDir?: string) => SessionManager; forkFrom: (sourcePath: string, cwd: string, sessionDir?: string) => Promise; open?: (sessionFile: string, sessionDir?: string, cwdOverride?: string) => SessionManager; }; /** * Test-only override for runners directory. * Defaults to `/.pi/agent/orca/runners`. */ runnersDir?: string; /** When set, reuse the existing session file instead of fork/fresh. */ respawn?: SpawnRespawnSpec; } export interface SpawnTmuxResult { agentId: string; status: "running"; isolation: "tmux"; pid: undefined; sessionPath: string; tmuxWindowId: string; tmuxPaneId: string; tmuxTarget: "pane" | "window"; scriptPath: string; } /** * Spawn a tmux agent. * Creates a new tmux window running a wrapper shell script that execs the * captured Pi binary with the appropriate flags and pipes the task via stdin. * * @param params - Spawn parameters * @returns Result with agentId, status, isolation, sessionPath, tmuxWindowId, scriptPath */ export declare function spawnTmuxAgent(params: SpawnTmuxParams): Promise; /** * Stop a tmux agent by killing its window. * Spec: §5.1.1 — tmux: spy on `tmux kill-window` * * @param agentId - Agent ID * @param exec - Optional exec override for testing * @returns True if found and killed */ export declare function stopTmuxAgent(agentId: string, exec?: typeof execFileAsync): Promise; /** * Stop all tmux agents. * Spec: §5.1.1 stopAllAgents dispatcher * * @param exec - Optional exec override for testing */ export declare function stopAllTmuxAgents(exec?: typeof execFileAsync): Promise; /** * Drive the next prompt cycle of a persistent tmux agent. * Spec §5.13. * * Read-merge-writes status YAML to `running` then pushes an `agent-control: * prompt` notify message to the per-child socket. The child's notify * handler invokes `pi.sendUserMessage(body)` to start the turn. * * Returns `false` when the agent isn't registered, isn't persistent, lacks * a socket path, or the notify push fails. */ export declare function sendPromptToTmuxAgent(agentId: string, task: string, notifyFn?: typeof notifySession): Promise; export {}; //# sourceMappingURL=tmux.d.ts.map