/** * CodingHarness interface — abstraction for TUI-based coding agents. * * Each harness implementation encapsulates the full lifecycle of a TUI * coding agent running inside a tmux window: environment setup, command * construction, readiness detection, completion monitoring, and cleanup. * * The spawner dispatches through this interface, making it possible to * route each agent role to a different harness (pi, Command Code, etc.) * without changing the shared launch orchestration. */ import type { EffectiveRoleDefinition } from "#src/role-definition"; import type { TmuxManager } from "#src/tmux-manager"; import type { SubagentRecord, SubagentSettings } from "#src/types"; /** * Pi/cmd default agent-scoped lifecycle trio (status jsonl, ready file, end * sentinel). Used when a harness omits `resolveOwnedTempPaths` and by pi/cmd * themselves. * * With `runtimeDir` present (a new-layout record), the trio lives inside the * owned per-agent runtime directory under semantic basenames. With it absent * (a legacy pre-runtime-storage record), the historical ambient-`$TMPDIR` * `pi-*` filenames apply — see the `subagent-runtime-storage` capability. */ export declare function defaultPiOwnedTempPaths(agentId: string, runtimeDir?: string): string[]; /** * Minimal session-analysis shape consumed by `LifecycleController`. * * Both `pi` and `cmd` harnesses have their own richer analysis types * (see `src/session-parser.ts`); this is the common subset that * `LifecycleController` reads to detect completion, count entries, and fire * turn thresholds. */ export interface SessionAnalysis { /** Whether the subagent has completed its task. */ completed: boolean; /** The extracted result text, if completed. */ result?: string; /** Number of entries parsed from the session file. */ entryCount: number; /** Number of assistant turns detected. */ turnCount: number; /** * Context usage (tokens) from the most recent assistant message's usage * data. Present for harnesses whose session format carries usage (pi); * absent otherwise (cmd) — the widget's health bar degrades gracefully. */ contextTokens?: number; /** * The model's REAL context window (tokens) when the session format carries * it (codex `token_count.info.model_context_window`). Preferred over the * static per-model table for the widget health bar's denominator; absent → * the table applies as before. */ contextWindow?: number; /** * Names of tool calls in the most recent tool-bearing assistant turn. Used * by `LifecycleController` to surface `record.currentTool` (the widget's * cast lane) for harnesses WITHOUT a live status stream (see * {@link CodingHarness.providesToolStatusStream}). */ toolCalls?: string[]; } /** * Declarative descriptor for a harness's config-dir shape, consumed by core's * generic `synthesizeConfigDir()` (see the `harness-config-dir` capability). * * The descriptor carries ONLY data — paths and an env-redirect builder. The * merge / union / symlink logic lives ONCE in core (`src/config-dir-synth.ts`), * never per-harness. Adding a harness that supports a config-dir override means * declaring this data, not re-implementing filesystem logic. * * The manifest (`mergeFiles` / `unionDirs` / `authFiles`) is FIXED per harness * in v1 (not user-extensible). * * A harness WITHOUT a `configProfile` does not support a config-dir override: * the extension warns and ignores any config-dir configured for a role using * that harness (D2). */ export interface HarnessConfigProfile { /** * The base config dir the layer merge reads FROM. pi: * `PI_CODING_AGENT_DIR ?? ~/.pi/agent`; cmd: `os.homedir()`. */ baseConfigDir(): string; /** * Subdirectory WITHIN the synth dir that holds config, relative to the redirect * target. pi: `"."` (the synth dir IS the agent dir). cmd: `".commandcode"` * (the synth dir is shaped as a HOME containing `.commandcode/`). */ configSubdir: string; /** * JSON config files (relative to `configSubdir`) deep-merged base ⊕ overlay * in layer mode (overlay keys win; arrays replaced wholesale). e.g. pi: * `settings.json`, `models.json`; cmd: `settings.json`. */ mergeFiles: string[]; /** * Resource directories (relative to `configSubdir`) unioned base ⊕ overlay in * layer mode (overlay wins on name collision). e.g. `skills`, `agents`. */ unionDirs: string[]; /** * Auth/credential files (relative to `configSubdir`) provided in the synth dir * FROM the base (symlink by default) in layer mode. e.g. pi: `auth.json`. */ authFiles: string[]; /** * The environment assignment that points the harness's CLI at `synthDir`. * pi: `{ PI_CODING_AGENT_DIR: synthDir }`; cmd: `{ HOME: synthDir }`. */ redirectEnv(synthDir: string): Record; } /** * Parameters passed to a harness's lifecycle methods. * * These are the per-spawn inputs that every harness needs to set up, * launch, and monitor an agent. Fields common to all harnesses live * at the top level; harness-specific data goes into `metadata`. */ export interface HarnessSpawnParams { /** The agent type / role name (e.g. "implementer", "explore"). */ agentType: string; /** Unique agent ID (UUID v4) for this spawn operation. */ agentId: string; /** The task prompt for the subagent. */ prompt: string; /** Immutable definition payload when this new spawn has one. */ roleDefinition?: EffectiveRoleDefinition; /** Optional short description (3-5 words) shown in the UI. */ description?: string; /** Resolved model identifier (e.g. "deepseek-v4-flash"). */ model?: string; /** Resolved provider identifier (e.g. "opencode-go"). */ provider?: string; /** Thinking level override ("off", "low", "medium", "high", "xhigh"). */ thinking?: string; /** Maximum agentic turns before forced stop (0 = unlimited). */ maxTurns?: number; /** Whether to prepend parent conversation context. */ inheritContext?: boolean; /** Pre-formatted parent context text (for inheritContext). */ parentContextText?: string; /** Backend to use: "spawn" or "tmux". */ backend?: string; /** Restart policy: "permanent", "transient", "temporary". */ restartPolicy?: string; /** Whether to enable the child-status-writer extension for tool visibility. */ statusFileEnabled?: boolean; /** Merged subagent settings (extensions, env, etc.). */ settings?: SubagentSettings; /** * Optional explicit branch name for git worktree isolation. * Mirrors `SpawnParams.worktreeBranch` so harnesses can see it. */ worktreeBranch?: string; /** * Path to the pi config directory (PI_CODING_AGENT_DIR or ~/.pi/agent). * Used for resolving config files and session storage. */ configDir: string; /** * Parent session identifier, used for tmux session naming and * config directory scoping. */ parentSessionId: string; /** * Resolved config-dir override for this spawn, produced by the spawner via * `synthesizeConfigDir()` (see the `harness-config-dir` capability). Carries * the redirect target `dir` (a layer synth dir or the replace target), the * `env` that points the harness's CLI at it (`redirectEnv`), and the `mode`. * * Absent → no override: the harness prepares its config exactly as today. */ configDirOverride?: { dir: string; env: Record; mode: "layer" | "replace"; }; /** * Core-allocated owned per-agent runtime directory * (`/tmp/subagents///runtime` — see the * `subagent-runtime-storage` capability). Created by the spawner BEFORE * `setupEnvironment()`. A built-in harness places all of its per-agent * lifecycle files (status jsonl, ready file, end sentinel, disposable * mirror) inside it and returns the same path as * `HarnessEnvironment.runtimeDir`. Independent of any config-dir override * (layer B). Absent only on legacy/test call paths — the harness then * falls back to its historical ambient-`$TMPDIR` layout. */ runtimeDir?: string; /** * Arbitrary harness-specific data. Each harness defines its own * keys to avoid polluting the shared shape. */ metadata?: Record; } /** * Typed key-value bag produced by `setupEnvironment()` and consumed * by subsequent lifecycle methods. * * Known fields are defined as optional properties; harness-specific * data (temporary file paths, hook scripts, etc.) goes into `metadata`. */ export interface HarnessEnvironment { /** Directory where the harness stores session files and config. */ sessionDir: string; /** Path to the JSONL session file being monitored for completion. */ sessionFilePath?: string; /** Path to the agent's config directory (e.g. for pi settings.json). */ configDir?: string; /** * Env assignments that redirect the launched harness at its config dir, from * a config-dir override (`HarnessConfigProfile.redirectEnv`). The spawner * prepends these to the launch command as a subshell-scoped env prefix. * Absent → no override (nothing prepended; launch command unchanged). */ redirectEnv?: Record; /** * Path to the per-agent status file (.jsonl) written by the * status-writer extension / hook for real-time tool visibility. */ statusFilePath?: string; /** * Path to the readiness signal file. The harness polls this file * to detect when the TUI is ready to receive input. */ readyFilePath?: string; /** * Path to a hook-audit file (cmd-specific) that provides structured * tool execution data during monitoring. */ hooksAuditPath?: string; /** * Path to the per-agent lifecycle-end sentinel file. The harness's * completion mechanism (pi's `agent_end` extension event / cmd's * `Stop` hook) writes this file the instant the agent finishes its * agent loop, independent of tmux window death or TUI close. * * `LifecycleController` gates completion on `harness.isComplete(env)` * (which reads this sentinel), NOT on session-file inference. See * the `completion-sentinel` capability. * * Populated by `setupEnvironment()`. */ endFilePath?: string; /** * The core-allocated owned per-agent runtime directory this spawn's * lifecycle files live in (mirrors `HarnessSpawnParams.runtimeDir`; see the * `subagent-runtime-storage` capability). Built-in `setupEnvironment()` * implementations return the exact core-supplied value; every * tmux-pilot-owned lifecycle file is a descendant of it. Absent on legacy * environments whose lifecycle files live under ambient `$TMPDIR`. */ runtimeDir?: string; /** * The tmux recovery identifier in the form `:`. * Set by the spawner after window creation, consumed by monitoring/cleanup. */ recoveryId?: string; /** * Absolute path to the git worktree this agent operates in, or undefined * if running in the main repo. Set by the spawner during the worktree * creation phase. Harnesses can use this to scope CWD or cleanup. */ worktreePath?: string; /** * Branch name checked out in the worktree, or undefined for main repo. * Set by the spawner. When `worktreeEphemeral` is true, the worktree * will be removed on agent completion. */ worktreeBranch?: string; /** * True if the worktree was auto-generated from a description (vs. an * explicit user-supplied branch). Ephemeral worktrees are cleaned up * on agent completion; explicit ones persist. */ worktreeEphemeral?: boolean; /** * Harness-specific data. Each harness defines its own keys. */ metadata?: Record; } /** * Abstract lifecycle of a TUI-based coding agent (coding harness). * * Lifecycle (called by the spawner dispatcher in order): * 1. setupEnvironment() — prepare files, inject configs, return paths * 2. buildLaunchCommand() — construct the CLI command to start the TUI * 3. waitForReady() — poll readiness signal, discover session file * 4. cleanup() — remove temp files, restore modified configs * * After step 2, the spawner creates the tmux window using the launch command. * After step 3, the spawner sends the prompt via tmux send-keys. * * Completion is NOT a harness responsibility: the `LifecycleController` / * `LifecycleController` polls `analyzeSession()` and gates completion on * `isComplete()` (the lifecycle-end sentinel). */ export interface CodingHarness { /** * Unique identifier for this harness (e.g. "pi", "cmd"). * Used as the key in HarnessRegistry and in tmux-pilot.config.yaml. */ readonly id: string; /** * Declares how a resolved Markdown role definition reaches this harness. * Core dispatches solely through this strategy, never through a harness id. */ readonly roleDefinitionDelivery: "prompt-context" | "native" | "unsupported"; /** * Native definition staging hook. Required when * `roleDefinitionDelivery` is `"native"`; invoked before the TUI launch. */ prepareRoleDefinition?(definition: EffectiveRoleDefinition, params: HarnessSpawnParams): Promise; /** * Optional self-describing widget identity — the harness's party/raid frame * color (`#rrggbb`) and sigil (a single grapheme). DECLARATIVE data only. * * When declared, the composition root publishes it into the widget's runtime * identity overlay at registration, so the host-neutral resolvers * (`harnessColorHex` / `harnessSigil`, which take a harness-id STRING) resolve * it with precedence `declaration > core token map > neutral fallback`. When * absent, the harness resolves via the core maps exactly as before. The core * maps are retained as the fallback layer and as the reservation table for * future harnesses that have no class yet. Declaring this does NOT couple the * harness to host/widget code (see the `self-describing-harness-identity` * capability). */ readonly widgetIdentity?: { colorHex: string; sigil: string; }; /** * Optional default tmux key combo used to stop a runaway agent of this * harness when no per-harness/per-role override is configured. DECLARATIVE * data only. Resolution precedence is `harnessSettings YAML override > * this declaration > DEFAULT_HARNESS_SETTINGS[id] > "C-c"`. When absent, the * harness falls back to the core `DEFAULT_HARNESS_SETTINGS` map (retained). */ readonly defaultStopKeyCombo?: string; /** * Optional one-line human description of this harness's lifecycle-end * completion sentinel (e.g. "Stop-hook sentinel"). DECLARATIVE data only — * surfaced read-only on the web console's harness identity summary. When * absent, the console shows nothing for the sentinel kind. */ readonly completionSentinelKind?: string; /** * Optional declarative config-dir descriptor. When present, the harness * supports a resolved config-dir override (overlay/replace) driven by core's * `synthesizeConfigDir()`. When absent, the harness does NOT support an * override and the extension warns-and-ignores any config-dir configured for * a role using it (see the `harness-config-dir` capability, D2). Adding this * member does NOT change the semantics of any existing method. */ readonly configProfile?: HarnessConfigProfile; /** * Whether this harness emits a LIVE tool-status stream (per-tool * start/stop events) that the `StatusPoller` consumes to set * `record.currentTool` in real time — pi's child-status-writer extension * does this. Harnesses WITHOUT such a stream (cmd, whose hooks-audit file * carries only SessionStart/Stop, no tool events) leave this falsy, and the * `LifecycleController` instead derives `currentTool` from the session * file's parsed {@link SessionAnalysis.toolCalls}. Optional — absent means * false (no live stream). */ readonly providesToolStatusStream?: boolean; /** * How core prompt delivery must paste this harness's prompt into its TUI * (see `pastePrompt()` in `src/prompt-delivery.ts`). * * - `"literal"` — always paste with literal `send-keys -l`, regardless of * prompt length or newlines. Required by harnesses (cmd) whose TUI * collapses a bracketed paste into an unexpanded attachment chip, silently * dropping the prompt. * - `"adaptive"` — use the length-based heuristic (short + no newline → * literal, else bracketed-paste `send-keys` long path). * * Optional — absent means `"adaptive"` (pi's behavior). */ readonly promptDeliveryStyle?: "literal" | "adaptive"; /** * Whether confirmed prompt delivery may re-paste ONCE when its first * confirmation window expires, no session file exists, and capture-pane * proves the prompt marker is absent. Codex v0.144.6 can silently discard * the first startup paste; other harnesses retain paste-once semantics. */ readonly repastePromptIfMissing?: boolean; /** * Whether the resume path must deliver prompt text and Enter in a SINGLE * tmux command (`sendKeysWithEnter*`) rather than as separate send-then-Enter * commands. Required by cmd; see the resume path in `src/tools/subagent.ts`. * * Optional — absent means `false` (pi's send-then-Enter behavior). */ readonly combinesTextAndEnter?: boolean; /** * Deterministically reconstruct this harness's session directory from a * persisted `SubagentRecord`, without re-running `setupEnvironment()` (which * would recreate config/hooks). Called by `src/startup-reconciler.ts`. * * This method is OPTIONAL — when absent (or when the harness can't be * resolved from the registry at all), the reconciler falls back to pi's * default, `join(record.configDir, "sessions")`. * * @param record - The persisted record to reconstruct the session dir from */ resolveSessionDir?(record: SubagentRecord): string; /** * Reconstruct the lifecycle-end sentinel path for a known agent id. * * Resume, startup-reconcile, and GC/teardown need this without re-running * `setupEnvironment()`. Callers pass the record's persisted `runtimeDir` * when present (a new-layout record): built-in implementations then resolve * the semantic filename inside it (`/end`); with it absent (a * legacy record) they resolve their historical ambient-`$TMPDIR` filename. * Default convention (when the method is omitted) is * `/end`, or `$TMPDIR/pi-tmux-end-` for legacy records * (pi + cmd). Harnesses that use a different layout (hermes, claude-code) * MUST implement this or completion detection after resume/reconcile * watches the wrong file. */ resolveEndFilePath?(agentId: string, runtimeDir?: string): string; /** * Reconstruct a deterministic session/mirror file path for a known agent id. * * Optional. When present, resume/reconcile seed `env.sessionFilePath` * directly instead of discovering via `waitForSessionFile` (which only * finds pi-style `*_.jsonl` names). Hermes needs this for its * plugin-written mirror. `runtimeDir` selects the current * (`/session-mirror.jsonl`) versus legacy (`$TMPDIR`) layout, * exactly as in {@link resolveEndFilePath}. */ resolveSessionFilePath?(agentId: string, runtimeDir?: string): string; /** * Late-binding session-file discovery for harnesses whose session file has * NO deterministic path and does NOT exist at ready time (codex: the * rollout `sessions/YYYY/MM/DD/rollout--.jsonl` under the * per-spawn home is created only when the first prompt is submitted). * * Called repeatedly (cheap, synchronous, non-throwing) by prompt delivery's * confirmation loop and by `LifecycleController`'s session-file wait — * within the existing `sessionFileTimeoutMs` budget — until it returns a * path. Returns `undefined` while the file does not exist yet. * * Optional — absent means the pi-pattern `waitForSessionFile` discovery * applies (or the harness sets `env.sessionFilePath` in `waitForReady`). */ discoverSessionFile?(env: HarnessEnvironment): string | undefined; /** * Agent-scoped disposable temp/sentinel files that teardown/GC may unlink * for this agent (end sentinel, ready file, status jsonl, disposable * session mirror, …). * * With `runtimeDir` present, built-in implementations return only lifecycle * files inside that owned runtime directory; with it absent they return * their historical ambient-`$TMPDIR` paths for legacy records (see the * `subagent-runtime-storage` capability). * * MUST NOT include permanent install artifacts (global hooks, plugins, * user config dirs) or worktrees/windows. When `resolveEndFilePath` is * implemented, its result for the same arguments MUST appear in this list; * same for a disposable `resolveSessionFilePath` mirror. * * Optional — omitted → consumers use {@link defaultPiOwnedTempPaths}. */ resolveOwnedTempPaths?(agentId: string, runtimeDir?: string): string[]; /** * Prepare the environment before launching the TUI. * * Creates configuration files, injects hooks, sets up directories, * and returns a HarnessEnvironment with all the paths and metadata * that later lifecycle methods will need. * * @param params - Parameters for this spawn operation * @returns A promise that resolves with the populated environment bag */ setupEnvironment(params: HarnessSpawnParams): Promise; /** * Post-worktree preparation hook. * * The spawner runs `setupEnvironment()` (Phase 1) BEFORE it creates the git * worktree (Phase 2), so `env.worktreePath` is only populated AFTER * `setupEnvironment()` returns. A harness that needs to act on the worktree * path BEFORE the TUI launches — e.g. Claude Code's folder-trust pre-seed, * which must write `projects[].hasTrustDialogAccepted` into * `$CLAUDE_CONFIG_DIR/.claude.json` before `claude` reads it at startup — * implements this method. The spawner calls it (when present) between * worktree creation and `buildLaunchCommand()`. * * This method is OPTIONAL — harnesses that don't need post-worktree * work omit it (the spawner checks via `if (harness.prepareAfterWorktree)`). * * @param env - The environment bag; `env.worktreePath` is now populated * when a worktree was created. * @returns The (possibly mutated) environment bag. */ prepareAfterWorktree?(env: HarnessEnvironment): Promise; /** * Build the tmux launch command string. * * Returns the bare TUI command WITHOUT the prompt — the prompt is * delivered via tmux send-keys after waitForReady() signals readiness. * * @param params - Parameters for this spawn operation * @param env - The environment bag from setupEnvironment() * @returns The shell command string to execute inside the tmux window */ buildLaunchCommand(params: HarnessSpawnParams, env: HarnessEnvironment): string; /** * Wait for the TUI to be ready to receive input. * * Polls the readiness signal file (env.readyFilePath) until the * TUI signals readiness. During this phase, the harness should * also discover and populate the session file path. * * @param params - Parameters for this spawn operation * @param env - The environment bag (mutated in place or returned updated) * @param agentId - The unique agent ID for this spawn * @param tmux - TmuxManager instance for any tmux interactions needed * @param signal - Optional abort signal to cancel waiting * @returns A promise that resolves with the (possibly updated) environment */ waitForReady(params: HarnessSpawnParams, env: HarnessEnvironment, agentId: string, tmux: TmuxManager, signal?: AbortSignal): Promise; /** * Clean up resources after completion or failure. * * Removes temporary files, restores modified configuration files, * and releases any harness-specific resources. * * This method is OPTIONAL — harnesses that don't need cleanup can * omit it (check via `if (harness.cleanup)` before calling). * * @param env - The environment bag with paths to clean up */ cleanup?(env: HarnessEnvironment): Promise; /** * Read the current turn count from the agent's session file. * * Returns the number of assistant turns detected in the session * file, or `undefined` if no session file path is set. * * This method is OPTIONAL — harnesses that don't support reading * turn counts can omit it (check via `if (harness.readTurnCount)` * before calling). * * @param env - The environment bag with the session file path * @returns The turn count, or undefined if unavailable */ readTurnCount?(env: HarnessEnvironment): Promise; /** * Analyze the agent's session file using the harness-specific parser. * * `LifecycleController` calls this on every poll cycle to detect completion * and entry-count changes. Each harness MUST implement this with * the correct parser for its session format: * - `PiHarness.analyzeSession()` → `analyzeSessionFile()` (pi JSONL) * - `CmdHarness.analyzeSession()` → `analyzeCmdSessionFile()` (cmd JSONL) * * This method MUST throw on read errors so `LifecycleController` can route * the failure through its staleness branch. * * **Note:** The `completed` flag in the returned `SessionAnalysis` is * NON-authoritative for `LifecycleController` completion gating — completion * is gated on `isComplete()` (the lifecycle-end sentinel) instead. * `analyzeSession()` is retained for progress tracking (entry/turn * counts), staleness detection, and result extraction. * * @param filePath - Absolute path to the session JSONL file * @returns The session analysis with completed/result/entryCount/turnCount */ analyzeSession(filePath: string): SessionAnalysis; /** * Report whether the harness's per-agent lifecycle-end completion * sentinel has been written. * * `LifecycleController` calls this on every poll cycle and declares the * agent completed only when `isComplete()` returns `true` AND a * result is extractable from `analyzeSession()`. This is the * authoritative completion signal — see the `completion-sentinel` * capability. * * MUST be synchronous and cheap (a filesystem stat/read of * `env.endFilePath`). Returns `false` when the sentinel is absent * or `endFilePath` is unset. * * @param env - The environment bag carrying `endFilePath` */ isComplete(env: HarnessEnvironment): boolean; } //# sourceMappingURL=interface.d.ts.map