Pane Commands
September 4, 2026 · View on GitHub
Domain: runtime
Overview
fab pane is the parent command grouping nine tmux-pane operations. Six query, manipulate, or sweep existing panes: map enriches a pane enumeration — sourced from rk mux panes --json when rk is present, fail-open to its own tmux list-panes — with fab-specific state (worktree, change, stage resolved from per-pane CWD), capture and process shell out to tmux to query or manipulate panes with the same enrichment, window-name is a primitive set for idempotent / guarded rewrites of the tmux window name — used by /fab-operator to mark enrolled and done-monitoring windows — kill is the generic pane-removal verb, and questions sweeps candidate panes for pending questions/prompts (fab-operator's §5 question-detection policy mechanized into the binary — a first-class skill-facing verb, the named exception to the rk-twins fence below). The other three — open, ready, deliver — are the provider-generic spawn / readiness-probe / verified-delivery primitives, addressed by pane id (or provider name) with no fab context required; fab dispatch open/ready/deliver are thin record-keeping bindings over them (see dispatch.md). Sending keystrokes to an agent pane and waiting on one are not fab verbs: agent messaging rides run-kit's rk mux send / rk mux await when rk is installed (command -v rk-gated, fail-open to raw tmux send-keys plus manual probing) — see agent-primitives.md. capture, process, and kill are fab verbs but dispatch-internal (cli-layering Part 7): kept for the rk-less pane arm — the dispatch orchestrator's peek/escalation path, fab dispatch logs' suggested capture command, and probe cleanups — while skill-facing peek, process-tree inspection, and pane removal ride run-kit's substrate twins rk mux capture / rk mux process / rk mux kill (rk-gated, raw-tmux fail-open; usage owned by agent-primitives.md § Peek).
The command group runs from any directory — including outside a fab-managed repo (scratch tmux tabs, cross-repo orchestration, non-fab daemons). The router routes every non-fab-kit command to fab-go regardless of config.yaml presence, and pane subcommands carry no resolve.FabRoot() guard because they resolve state from target pane IDs rather than from the invoker's CWD. See kit-architecture.md for the router's always-route policy.
This doc covers the eight subcommands, the --server / -L persistent flag, and the semantic invariants that govern how pane IDs and server selection interact with tmux's own socket model.
Requirements
Parent Command: fab pane
fab pane is a cobra command group with nine subcommands (map, capture, process, window-name, open, ready, deliver, kill, questions) and one persistent flag (--server / -L). Invoking fab pane with no subcommand prints the standard cobra help listing the nine subcommands. Source: src/go/fab/cmd/fab/pane.go.
Subcommand: fab pane map
fab pane map [--json] [--session <name>] [--all-sessions] combines pane enumeration with worktree/change/runtime state into a unified view. Source: src/go/fab/cmd/fab/pane_map.go.
Enumeration is delegated to run-kit when present (cli-layering Part 8): map sources its pane list from rk mux panes --json (appending -L <server> when the flag is set) and keeps only the choreography-enrichment role — rk owns the substrate enumeration. The delegation is capability-probed and fail-open: ANY failure — rk absent from PATH (exec.LookPath), a pre-3.17.18 rk without the subcommand, a non-zero exit, unparseable JSON, or a failed current-session lookup in default mode — falls back silently to fab's own tmux list-panes enumeration (the attempt IS the probe; no version check, never an error, no warning). Row mapping: pane←pane, tab←window_name, cwd←cwd, session←session, index←window_index, windowID←window_id; rk-only fields (session_id, window_active, pane_index, pane_active) are ignored, while the rk row's command is carried snapshot-internally on the fab row (never rendered as a column or JSON key — consumed by the operator tick's agent_exited predicate; see operator.md). Agent state on the delegated path is rk's reconciled agent_state/agent_state_duration taken structurally — the @rk_pane_agent_state option is never re-read there; a state token outside {active, waiting, idle} maps to unknown, and a waiting row's duration is dropped (fab's agent_idle_duration keeps its published idle-only semantics). Session scoping is fab's own row filter in both cases — rk enumerates the whole server: default mode filters to the current session (resolved via tmux display-message -p '#{session_name}', honoring --server; the $TMUX guard stays), --session <name> filters on equality, --all-sessions applies no filter. Two deliberate deltas on the delegated path: the row set is rk's filtered view (_rk-pin-* pin-sessions and the _rk-ctl anchor excluded; a pinned window listed once via its home session — the fallback keeps the raw view, fab never reimplements rk's filtering conventions), and an unknown --session name filters to zero rows (No tmux panes found., exit 0) instead of tmux's unknown-target error. Everything else — table columns, the JSON field set and nullability, the per-cwd enrichment pipeline — is identical on both paths. The delegation seams (rkPanesRunner, currentSessionName) are injectable package-level vars for deterministic fallback-trigger tests.
Flags:
| Flag | Type | Purpose |
|---|---|---|
--json | bool | Output as JSON array instead of aligned table |
--session <name> | string | Target a specific tmux session by name (skips $TMUX check) |
--all-sessions | bool | Query all tmux sessions (skips $TMUX check) |
--server <name> | string | Persistent flag — see §--server flag below |
--session and --all-sessions are mutually exclusive. When neither is set, discovery runs against the current tmux session only (tmux list-panes -s) and requires $TMUX to be set.
Table columns: Session (only with --all-sessions), Pane, WinIdx, Tab, Worktree, Change, Stage, Agent. The Worktree column displays (main) for the main worktree, a relative path from the main repo's parent for other git worktrees, or basename/ for non-git panes. Non-fab panes render em-dash fallbacks for Change and Stage. The Agent column is not a non-fab em-dash case: it resolves from the pane's @rk_pane_agent_state option for every pane class (including non-git / non-fab panes), so it renders em-dash only when that option is absent or unparseable — not because the pane lacks fab context (see Agent state resolution below). The relative Worktree path is computed per repo — each pane's display path is relative to its own repo's main-worktree root, so panes from different repos render correct paths (no human-table Repo column is added).
JSON fields (snake_case): session, window_index, window_id, pane, tab, worktree, repo, change, stage, display_state, agent_state, agent_idle_duration, pr_url, pr_number. repo is the absolute main-worktree root for the pane's repo, null when unresolved (non-git pane); it is exposed in --json ONLY (no human-table column) so programmatic callers can group rows by repo without re-deriving. change and stage are null when no active change exists on the pane's worktree. agent_state and agent_idle_duration populate from the enumeration source — rk's reconciled values on the delegated path (a waiting duration is dropped), the pane's @rk_pane_agent_state option on the fallback — independent of change / stage. agent_state ∈ {active, waiting, idle, null} (waiting (ioku); null when unknown); agent_idle_duration is populated only for idle (null for active/waiting/unknown). The JSON is emitted from the structured (state, duration) pair carried on the row from discovery time, not by re-parsing the human display string, so a display-format tweak cannot silently break the JSON contract.
display_state (*string, placed immediately after Stage in paneJSON) is the state half of status.DisplayStage (the stage field is the name half). Values: active, ready, done, failed, pending, skipped, or null. It is null exactly when stage is null (no resolvable change, no fab/ dir, or an unloadable .status.yaml) — the same em-dash-sentinel → toNullable() nullability contract as repo/change/stage. Exposed in --json ONLY — no human-table column; the table output is byte-identical to the pre-field rendering. Why: the stage name alone cannot distinguish an actively-worked stage from a parked finished change — a fully-shipped change renders "stage": "review-pr" indefinitely until archived, byte-identical to a change whose review-pr is actively running; the pair (review-pr, done) vs (review-pr, active) disambiguates. Additive shape: existing JSON consumers ignore unknown keys, matching the repo (h3jk) and pr_url/pr_number (r7ju) precedent. Consumer: the run-kit sidebar (app/backend/internal/sessions/sessions.go paneMapEntry) opts in separately in its own repo — it gains honest per-row attention states (failed/ready = needs human, done + parked = quiet row) instead of heuristics over agent_state / .fab-runtime.yaml idle_since. See change-lifecycle.md "Deriving display stage" for the tier walk that produces the value (including the failed tier added alongside this field).
window_id (*string, placed immediately after window_index in paneJSON) is the tmux #{window_id} (@N) — the server-assigned window identifier that is stable for a window's lifetime and travels with the window across swap-window/move-window. It is null only for a legacy/absent input line (a pre-change tmux format string, or a shorter line that omits the field) — a live window always has one, independent of git/fab context (like agent_state, it is resolved for every pane class, including non-git rows). It is emitted via the same em-dash/empty-sentinel → toNullable() nullability contract as repo/display_state/pr_url, and exposed in --json ONLY — no human-table column (the table keeps only WinIdx), so table output is byte-identical to the pre-field rendering. Why: the positional (session, window_index) pair is not a stable window identity — a tmux swap-window reorder reassigns indexes, so a consumer that joins pane-map rows to its own tmux snapshot by index misattributes one window's fab status to whichever window now occupies that index. window_id gives consumers a stable join key that survives reorders, removing the positional coupling. Consumer: run-kit's sidebar join saw exactly this bug — it caches the pane map for 5s, so after a swap-window the stale index join lagged the reorder for up to the cache TTL (a sidebar status dot attached to the wrong window); joining on window_id fixes it for that and every other index-joining consumer. Additive shape: existing JSON consumers ignore unknown keys, matching the repo (h3jk), display_state, and pr_url/pr_number (r7ju) --json-only precedent; window_index is retained unchanged (backward compat). Introduced by 260713-ueuy.
Identity-key contract (declared in fab pane map --help's Long text and _cli-fab.md § fab pane map): the row schema inherits run-kit's contract — rk mux panes --json is the primary declaration (a run-kit companion item); this command consumes and re-emits it. The identity keys are pane (the tmux %pane_id, meaningful only with its server socket context) and window_id. session and window_index are DISPLAY-ONLY — positional values reassigned by swap-window/move-window/session rename — and MUST NOT be used as join keys by any consumer; the run-kit StatusDot swap-lag above is the motivating misattribution. Consumers MUST tolerate window_id: ""/null — a legacy enumeration line carries none.
pr_url (*string) is the LAST entry of the pane's active change .status.yaml prs: list (most recent PR), sourced from the SAME sf.Load(statusPath) already performed for stage derivation — no second read. It is null when the prs: list is absent or empty, or when the pane has no fab change / no fab/ dir / unresolved git. pr_number (*int) is parsed from the URL's trailing /pull/<n> segment (via the parsePRNumber helper); it is null when there is no URL or the URL is unparseable — a malformed URL keeps pr_url set but yields pr_number: null. Like repo, both fields are exposed in --json ONLY — no human-table column is added. Deliberate non-goal: fab surfaces only the on-disk URL/number written by /git-pr; PR status (open/merged/closed, CI state) is NOT fab's job and there is no network / gh / git call — run-kit fetches live status separately. The toNullable helper now also nil-maps "" (alongside the em-dash and "(no change)" sentinels), so an empty prURL cleanly maps to JSON null; the pre-existing repo / change / stage callers are normalized to the em-dash sentinel and never pass "", so the new "" branch only fires for pr_url.
Per-repo mainRoot resolution: runPaneMap computes the main-worktree root per distinct repo, cached by the pane's GitWorktreeRoot via the mainRootForPane(cwd, wtRoot, cache) helper (one git worktree list lookup per repo, reused across that repo's panes). Each pane is resolved against its own repo's mainRoot, fixing the prior bug where one mainRoot derived from the first parsable pane was applied to every row — producing garbage relative paths for panes in other repos. paneRow carries a repo field set to that absolute root (em dash when unresolved); paneJSON.Repo is a nullable *string emitted via toNullable, matching the existing change/stage nullable-field pattern. Source: src/go/fab/cmd/fab/pane_map.go.
Subprocess economy (hot path — the operator's per-tick snapshot): each pane's git worktree root is resolved at most once per distinct pane cwd via the cwd-keyed worktreeRootForPane(cwd, cache) cache, with "" as the cached non-git sentinel that resolvePane's non-git branch keys off; the resolved root is threaded into both mainRootForPane and resolvePane (one git rev-parse --show-toplevel per pane cwd, not two). On the delegated path, enumeration is a single rk mux panes --json subprocess (plus one tmux display-message for the current-session name in default mode). On the fallback path, --all-sessions discovery is a single tmux list-panes -a -F <tmuxPaneFormat> call (the format carries #{session_name}) instead of list-sessions plus one list-panes -s -t <session> per session — -a enumeration also side-steps the prefix/glob target resolution of -t <session>.
Three-axis model: The map resolves three orthogonal axes independently — Change (from .fab-status.yaml), Agent (from the @rk_pane_agent_state tmux pane option), and Process (opt-in via fab pane process, not in map output). See runtime-agents.md for the full model.
Agent state resolution: On the delegated path the Agent column carries rk's reconciled state verbatim (see the enumeration-delegation paragraph above). On the fallback path it reads the agent-state pane option — carried on the existing list-panes -F format string as two adjacent tab-delimited fields, #{@rk_pane_agent_state} (6th, canonical) and #{@rk_agent_state} (7th, the legacy name read only when the 6th is empty), both possibly-empty MIDDLE fields ahead of #{window_id} as the never-empty 8th field (see window_id above); #{pane_current_command} follows as the never-empty-in-practice 9th/trailing field, carried snapshot-internally on the row as command for the operator tick's agent_exited predicate (never rendered as a column or JSON key — the rkPaneRow snapshot-internality design decision below); parsePaneLines tolerates legacy 5–8-field lines, which parse with command == "". Agent state therefore costs zero extra subprocesses there, and the tmux-server disambiguation problem does not arise because a pane option lives on exactly one server's pane. The raw option value is resolved at parse time (parsePaneLines) by the shared internal/pane helper (see runtime-agents.md): active → active; waiting → waiting; a well-formed idle:<epoch> → idle (<duration>) (duration from the epoch via FormatIdleDuration); absent/unparseable/unknown-token → —. Both paths land in the same structured (state, duration) pair on the row — one downstream render/JSON seam. This resolution is independent of whether the pane has an active change, and it is computed for every pane class (including non-git rows), so map agrees with capture on non-fab panes. See runtime-agents.md for the read contract, the three states + unknown, and epoch-derived duration.
Display scenarios:
| Scenario | Change | Stage | Agent |
|---|---|---|---|
| Change active, agent idle | 260417-... | apply | idle (2m) |
| Change active, agent active | 260417-... | apply | active |
| Change active, agent waiting on a human | 260417-... | apply | waiting |
| Discussion mode (fab worktree), agent idle | (no change) | — | idle (2m) |
| Change active, no instrumented agent | 260417-... | apply | — |
| Fab worktree, no change, no agent | (no change) | — | — |
Non-fab pane (no fab/ dir) | — | — | — |
Error behavior: Unset $TMUX with neither session flag → ERROR: not inside a tmux session (exit 1, returned through RunE to main's single formatter). No panes found → No tmux panes found. (exit 0).
Subcommand: fab pane capture
fab pane capture <pane> [-l N] [--json] [--raw] captures terminal content from a tmux pane with fab context enrichment. Dispatch-internal — skill-facing capture rides rk mux capture (see the Overview's dispatch-internal note). Source: src/go/fab/cmd/fab/pane_capture.go.
Flags: <pane> (required tmux pane ID, e.g. %5); -l/--lines (int, default 50) — returns the last N lines of the pane's content: the raw tmux capture-pane -p -S -N fetch (N scrollback lines + the entire visible screen) is tailed internally by the shared pane.TailLines helper, which strips the visible screen's trailing blank padding (empty or whitespace-only trailing rows) and takes the last N of what remains, preserving interior blank lines — so callers never need | tail -N; --json (structured output with pane metadata); --raw (captured text only, no header, no enrichment). --json and --raw are mutually exclusive.
Default output: Header block (pane ID, worktree, change, stage, agent state) followed by the captured text. The agent line reads @rk_pane_agent_state, falling back to the legacy @rk_agent_state when unset (same contract as map; see runtime-agents.md), and shows agent: <state> — e.g. agent: waiting, or agent: idle (<dur>) for a completed turn.
JSON output: pane, lines, content, worktree, change, stage, agent_state, agent_idle_duration — agent_state ∈ {active, waiting, idle, null} (same structured-source semantics as map --json; waiting (ioku)), agent_idle_duration only for idle. The worktree/change/stage fields are null when the pane is not in a fab worktree or has no active change.
Error behavior: Pane not found → Error: pane <id> not found (exit 2); any other tmux validation failure (dead server, bad socket) → exit 3 — the same scheme as window-name, so operator scripts can branch on cause uniformly across the pane family (260612-ye8r). --lines < 1 → ERROR: --lines must be >= 1 (exit 1, via RunE). Pane existence is validated via the targeted display-message probe (see §Shared Pane Package ValidatePane); a failed tmux capture-pane surfaces the child's trimmed stderr alongside the pane ID. --raw output is byte-identical to tmux's stdout within the returned window — the tail selects which lines are returned, but no byte inside them is trimmed (trailing spaces within a line survive; a non-empty window keeps tmux's per-line \n termination).
Copy-mode immunity: tmux capture-pane reads the pane's live grid, not the copy-mode viewport — a pane a human left scrolled up (#{pane_in_mode} = 1) still captures the live bottom, tracking new output while the human-visible display stays frozen (verified empirically on tmux 3.7c). Captures therefore need no pane-mode guard; the mode hazard is on the send path only (see § Shared Pane Package).
Subcommand: fab pane process
fab pane process <pane> [--json] detects the process tree running in a tmux pane via OS-level process inspection. Dispatch-internal — skill-facing process inspection rides rk mux process (see the Overview's dispatch-internal note). Source: src/go/fab/cmd/fab/pane_process.go (plus platform-specific pane_process_linux.go / pane_process_darwin.go).
Discovery: Linux reads /proc/<pid>/task/<tid>/children recursively; macOS uses ps -o pid,ppid,comm -ax with PPID traversal, plus ONE batched ps -axo pid=,args= pass parsed into a PID→args map (pure parsePSCmdlines parser: pid is numeric-first, remainder is args — robust against comm-with-spaces) and joined by PID for full cmdlines — exactly two ps spawns total, no per-node lookups. A process exiting between the two passes degrades to cmdline "" (the same value as a per-PID failure). Platform selection via Go build tags.
Classification is parameterized by an agent-name set built once per command. The set is claude/claude-code plus the basenames of the quote-aware leading command words from every merged provider's interactive_command (built-ins and project providers); true POSIX NAME=value prefixes are skipped, executable paths containing = remain commands, and known shells are excluded. A process matches agent by normalized comm first, then by the basenames of at most the first two cmdline tokens so interpreter-wrapped agents are recognized without scanning prompt arguments. Remaining node processes classify as node, git/gh as git, and all others as other. Project-config resolution is best-effort and degrades to the built-in provider table. The same parameterized classifier and recursive root-inclusive tree scan supply the operator tick's live-agent confirmation.
Default output: Tree-formatted process listing with PID, command name, and classification.
JSON output: pane, pane_pid, processes (tree of {pid, ppid, comm, cmdline, classification, children}), has_agent (true if any process classified as agent).
Platform-specific process discovery is tmux-server-independent — once the pane's shell PID has been resolved via GetPanePID, the /proc walk or ps traversal operates on the OS process table, not tmux.
Error behavior: Pane not found → Error: pane <id> not found (exit 2, in-handler via the typed *PaneNotFoundError branch); any other tmux validation failure (dead server, bad socket) → exit 3 — the pane-family scheme, shared with capture/ready/deliver.
Subcommand: fab pane window-name
fab pane window-name is a cobra subgroup with two verbs (ensure-prefix, replace-prefix) that perform guarded rewrites of the tmux window name. Both verbs read the current name via tmux display-message -p -t <pane> '#W', compare it against a literal prefix, and conditionally call tmux rename-window. Both honor the parent --server / -L flag via the existing WithServer argv builder. Source: src/go/fab/cmd/fab/pane_window_name.go.
Motivating use case: /fab-operator enrolls monitored windows with a » (U+00BB) prefix and transitions them to › (U+203A) on removal to keep the tmux tab bar an honest at-a-glance map of active vs. done monitoring.
Verb: ensure-prefix <pane> <char>
Idempotent prepend. Reads the current name; if it begins with the literal string <char>, no-ops. Otherwise runs tmux rename-window -t <pane> "<char><current-name>". Exits 0 on both rename and no-op, with stdout renamed: <old> -> <new> on rename and empty stdout on no-op.
<char> is any non-empty string — no width / BMP / codepoint validation is performed. The caller owns the single-width convention (the operator skill enforces it via its choice of » and ›).
Verb: replace-prefix <pane> <from> <to>
Atomic guarded swap. Reads the current name; if it begins with the literal string <from>, runs tmux rename-window -t <pane> "<to><name-without-from-prefix>". Otherwise, no-ops with exit 0 — this is the user-retitle-mid-monitoring guard: if the user retitled the window so it does not start with <from>, the swap is silently skipped.
<to> MAY be empty, in which case the <from> prefix is stripped (removal). <from> MUST be non-empty; an empty <from> exits 3 with a usage message on stderr.
Exit codes (both verbs)
| Exit | Meaning |
|---|---|
| 0 | Rename succeeded OR operation was a no-op |
| 2 | Pane does not exist — tmux stderr contains can't find pane (or no such pane). Stderr is propagated to the caller. |
| 3 | Any other tmux error: tmux not running / socket unreachable / rename failed / argument usage error (e.g., empty <char> or <from>). Stderr is propagated when tmux supplied it. |
The primitives do not gate on $TMUX; they rely on tmux's own exec failure to surface "tmux not running" as exit 3, which lets callers run them via --server targeting outside a tmux client. The distinct 2 vs. 3 split lets /fab-operator's removal path discriminate "pane gone" (exit 2 → treat as successful removal, window is gone anyway) from "pane alive but rename failed" (exit 3 → log warning and continue). Stderr mapping uses case-insensitive substring matching.
Subcommand: fab pane open
fab pane open --provider <name> [--role <role>] [-c <dir>] [--json] resolves the named provider's interactive_command — project config per-field merged over the built-in table, exactly as fab agent resolves it; outside a fab repo the built-in table alone applies — substitutes the {model}/{effort} fills via the standard precedence with the provider pinned at invocation time (agent.ResolveRoleWith with the provider override; --role selects whose fills apply, the default role otherwise — the opposite of fab agent --provider's deliberate fill bypass), and spawns the composed command in a tmux pane. It writes no dispatch record and no .fab-dispatch/ state. Source: src/go/fab/cmd/fab/pane_open.go.
Spawn shape: a plain split of the current window when the invoker is itself a tmux pane on the target server ($TMUX_PANE set, no --server), an unnamed new window otherwise — no size, no title, no worker-column placement (placement and the fab-{id}-{stage} identity are dispatch policy — see dispatch.md).
Output: opened pane %N (provider <name>), plus a server: <name> line when the socket is non-default. --json instead emits a single JSON object {"pane": "%N", "provider": "<name>", "server": null} — server is null for the default socket via the toNullable contract. Plain-text output is byte-identical without --json.
Error behavior: unknown provider → the shared lookup error naming the available providers (exit 1); a provider with no interactive_command → a hard error naming it (configure providers.<name>.interactive_command, exit 1 — the explicit-pane posture: no descent); unreachable tmux server or a failed spawn → exit 3 (reachability is probed via pane.ServerReachable before anything is spawned).
Subcommand: fab pane ready
fab pane ready <pane> runs the readiness classifier against the named pane id — the same gate fab dispatch ready binds over, with no dispatch record to load. Source: src/go/fab/cmd/fab/pane_ready.go.
The probe first reads the pane's foreground command: while it is still a shell — the provider binary has not taken the tty, and a cooked-mode shell echoes typed characters by itself, so the sentinel would echo with no agent ready — it reports booting and types nothing, and rk is never invoked on this path. Once a non-shell process owns the pane, the classification runs in two arms: when a sentinel-capable run-kit is on PATH (probed from rk mux await --help mentioning parked, cached once per process) it delegates to rk mux await --ready <pane> under a bounded internal timeout and maps rk's report (ready (state|echo) → ready, parked → parked, timeout running → booting, gone → the pane-missing error), failing OPEN to the raw arm with at most one stderr warning per process on any unexpected rk outcome; the raw-tmux arm (rk absent or not sentinel-capable) types the FAB-READY-PROBE sentinel literally (never submitted), checks the echo against two screen-stability captures, clears with C-u, and reports exactly one of ready / booting / parked — the classification table the two verbs share is documented under dispatch.md § fab dispatch ready. Non-ready reports add pane: %N, a server: <name> line when set, and the trailing capture snippet under --- last 20 lines --- (fab's own capture on both arms; no header over an empty snippet). All three classifications exit 0 — the report string is the sole discriminator. --json instead emits a single JSON object {"state": "<ready|booting|parked>", "pane": "%N", "server": null, "snippet": "…"} — snippet is the same trailing-blank-trimmed 20-line capture the text report carries ("" when empty), server is null when empty, and all three classifications still exit 0. Plain-text output is byte-identical without --json.
Side effect: the probe TYPES into the target pane once a non-shell process owns it (the sentinel — rk's own on the delegated arm, C-u-cleared before return on the raw arm) — a shell-foreground pane is only read, never typed into — so run it only against panes you own — never one an agent or a human is actively working in.
Error behavior: pane missing (including rk's gone on the delegated arm) → exit 2 (Error: pane <id> not found via ValidatePane); any other tmux failure (dead server, bad socket, failed probe) → exit 3 — the pane-family scheme.
Subcommand: fab pane deliver
fab pane deliver <pane> (--prompt-file <path> | --text <string>) runs the verified typed-delivery choreography against the named pane — the same gate fab dispatch deliver binds over, with no dispatch record and no completion signals to stash. Source: src/go/fab/cmd/fab/pane_deliver.go.
Per attempt: readiness probe → C-u → capture the cleared baseline → type the payload literally → wrap-tolerant echo-verify (countWrapped, whitespace + box-drawing runes dropped) → Enter → confirm the screen advanced, with exactly one retry (a retry warns on stderr even when it succeeds; a second failure prints the pane's last 20 lines to stderr and exits 1). Every keystroke rides the shared send seam's pre-send pane-mode guard (see § Shared Pane Package), so a pane a human left scrolled up in copy-mode is returned to its live tail before typing rather than eating the keys as mode bindings. --prompt-file checks the file exists first (missing → exit 1, nothing typed) and types the dispatch-parity pointer line Read <path> and execute it. (pane.PointerPrompt) — the path is typed as supplied (a generic pane's cwd is unknown to fab), so make it meaningful from the pane's own cwd; --text types its argument literally. The two flags are mutually exclusive and exactly one is required (usage error otherwise).
Output: delivered <pane> (prompt <path>) or delivered <pane> (text). --json instead emits a single JSON object on verified delivery — {"pane": "%N", "source": "prompt", "path": "<path>"} or {"pane": "%N", "source": "text"} (path is present only for prompt). Failures keep the stderr + non-zero contract and emit no JSON. Plain-text output is byte-identical without --json.
Error behavior: pane missing → exit 2; other tmux failure → exit 3; verification exhausted or a missing prompt file → exit 1 through RunE.
Subcommand: fab pane kill
fab pane kill <pane> kills a tmux pane — the generic, record-free exposure of the pane.KillPane helper with the family's validated exit-code contract. Dispatch-internal — skill-facing pane removal rides the agent-state-gated rk mux kill (see the Overview's dispatch-internal note); this verb backs rk-less probe cleanups and the pane arm. Source: src/go/fab/cmd/fab/pane_kill.go.
Validation: pane.ValidatePane first (the targeted display-message probe) — a missing pane prints Error: pane <id> not found and exits 2 in-handler via the *PaneNotFoundError branch; any other tmux failure (dead server, bad socket) exits 3 — the family scheme.
Output: killed <pane> on success, plus a server: <name> line when the socket is non-default (matching open's form).
Scope: generic and record-free — no dispatch-record interaction, no .fab-dispatch/ state. fab dispatch kill (record-keyed, ungated recovery) is unaffected and remains the pipeline's kill.
Subcommand: fab pane questions
fab pane questions [--all-sessions] [--panes <id>...] [--json] sweeps candidate panes for pending questions/prompts — fab-operator's §5 question-detection policy (the mechanical guards and indicator classes) mechanized into the binary. A first-class skill-facing verb, NOT dispatch-internal: the named exception to the rk-twins layering fence (the design decision below). Per candidate pane it re-reads state, captures the last 20 lines (fixed), applies the guards and the indicator scan bottom-most-first, and reports matches: (pane, agent_state, indicator, snippet) plus skipped: with one of five reasons (capture_failed, state_changed, blank_capture, turn_boundary, no_indicator). Discovery modes (--all-sessions, or the no-flag current-session default requiring $TMUX) sweep only waiting/idle panes; --panes takes IDs verbatim. Output is detection input only — never a license to send blind (the operator's pre-send gate and re-capture-before-send guard still run before any send). Exit codes follow map, not the per-pane 2/3 scheme: 0 on any clean sweep (matches/skips are data), non-zero only on usage error or hard discovery failure. The full contract (flags, JSON schema, indicator-class names, exit codes) is owned by _cli-fab.md § fab pane · questions; consumer policy lives in operator.md § Auto-Nudge. Source: src/go/fab/cmd/fab/pane_questions.go.
Usage-Error Coexistence with the Binary-Wide Exit-2 Convention (swon)
The pane-family 2/3 scheme above (2 = pane missing, 3 = other tmux failure, across capture/process/window-name/ready/deliver/kill) is set via an in-handler os.Exit from inside each verb's RunE. The fab/fab-go binary also has (swon) a binary-wide usage-error convention — 0 success / 1 operational failure / 2 usage error — where a usage error (an unknown/malformed flag, or a cobra arg-count violation on any pane verb) is caught at parse/validation time, before the handler runs, and exits 2 via the classifier in main()'s testable run() helper (execution-phase classification, no string matching — see kit-architecture.md § Binary-Wide Exit-Code Convention).
The two 2s coexist without renumbering: the in-handler pane os.Exit(2|3) calls bypass run()'s usage/operational mapping entirely, so the pane 2/3 scheme is unchanged — a missing pane still exits 2 in-handler, any other tmux failure still exits 3, and pane_exitcode_test.go stays green unmodified. On a pane verb, exit 2 is therefore intentionally ambiguous between "usage error" (at parse time) and "pane missing" (in-handler); the codes are not renumbered because downstream consumers (operator/run-kit scripts) branch on the pane codes. Disambiguate on stderr wording (Error: pane <id> not found for the in-handler case vs. cobra's usage/flag error text for the parse-time case). map and questions use plain ERROR:-formatted exit 1 for their in-handler errors (multi-pane sweeps have no single target pane to be "missing"; clean sweeps exit 0 with matches/skips as data) and are unaffected — a usage error on either likewise exits 2 at parse time under the binary-wide convention.
Output modes
Plain text is the default: renamed: <old> -> <new>\n on a rename, empty stdout on a no-op. The --json flag emits a single JSON object on stdout with the shape {"pane", "old", "new", "action"} where action is "renamed" or "noop". JSON output always emits an object (including for no-ops), unlike plain output which is empty on no-op. Matches the plain/--json pattern used by map and capture.
Operator skill consumption
src/kit/skills/fab-operator.md §4 Enrollment invokes fab pane window-name ensure-prefix <pane> » after writing the monitored entry to .fab-operator.yaml. §4 Removal invokes fab pane window-name replace-prefix <pane> » › on every removal path (terminal stage, stop_stage reached, pane death, explicit stop) — exit 2 is treated as successful removal; other non-zero exits log "{change}: window rename skipped ({error})." and continue.
--server / -L Flag
Registration: paneCmd registers a persistent string flag --server (short -L) with default "". Because it is a persistent flag on the parent, it is automatically visible on all eight subcommands' --help. Source: src/go/fab/cmd/fab/pane.go:14.
Help text: Target tmux socket label (passed as 'tmux -L <name>'). Defaults to $TMUX / tmux default socket.
Behavior:
- When the flag is absent or empty, every
exec.Command("tmux", ...)invocation in the pane call tree runs with no-Largument. Tmux inherits socket selection from$TMUX(when set) or falls back to its default socket. This is byte-for-byte identical to pre-flag behavior. - When the flag is non-empty, every
exec.Command("tmux", ...)invocation in the pane call tree is prepended with-L <value>. The flag is passed to tmux verbatim — fab does not inspect, validate, or normalize the server name. Tmux owns the semantics; any error (e.g.,no server running on /tmp/tmux-1001/nonexistent) is propagated to stderr.
Short form: fab pane map -L runKit is identical to fab pane map --server runKit.
Motivating use case: The run-kit daemon runs inside a tmux session named rk-daemon (so its $TMUX points to one socket) while the user's sessions it is inspecting live on a different socket (runKit). Without --server, fab pane map --json --all-sessions invoked by rk serve enumerates panes from the wrong socket — the one in its own $TMUX — and every key lookup misses. With fab pane map --json --all-sessions --server runKit, every internal tmux invocation runs with -L runKit and the correct pane set is returned. More generally, the flag enables any programmatic caller that needs to inspect a tmux server different from the one it inherits.
Workarounds that don't work (and why the flag is the right fix): Setting $TMUX as a socket selector is incorrect — $TMUX means socket,pid,pane_id, not a socket path, and some tmux code paths behave differently when $TMUX is set (e.g., refusing nested attach). $TMUX_TMPDIR only helps for default-named sockets in a dedicated tmpdir. Unsetting $TMUX and relying on the default socket only works when the target is in fact the default.
Semantic Invariants
Pane IDs are per-server. Tmux allocates pane IDs (e.g., %3, %5) within each tmux server's own scope. The same %3 can exist on two different servers and refer to unrelated panes. When --server <S> is passed with a pane ID argument, the ID is interpreted in the context of server <S>. Callers are responsible for pairing the correct pane ID with the correct server.
--server takes precedence over $TMUX. When both are set, the explicit CLI flag wins. This matches tmux's own behavior — tmux -L <label> explicitly selects a socket, overriding any inherited selection.
Non-tmux operations are unaffected by --server. File reads (.status.yaml), git-worktree detection (git rev-parse --show-toplevel, git worktree list), and OS-level process discovery (/proc on Linux, ps on macOS) key off the pane's CWD or the resolved folder name, not the tmux server. The --server value is never used as a filesystem lookup key. (Agent state is a tmux pane option, not a file read — so it does flow through the --server-prefixed tmux calls.)
Shared Pane Package (internal/pane)
Shared pane machinery lives in src/go/fab/internal/pane/ — the pane-resolution helpers in pane.go:
RunCmd(name string, args ...string) (stdout string, stderr []byte, err error)— the single subprocess-capture implementation for any child command (tmux, git, wt): captures stdout and stderr separately, returning stdout untrimmed so capture-style output is never alteredStderrError(err error, stderr []byte) error— appends the trimmed child stderr to an exec error when present (%w: <stderr>; returnserrunchanged when stderr is empty, the original error stays unwrappable viaerrors.Is/As), so failures surface the child's diagnostic — the agent self-correction signal — instead of a bareexit status 1IsPaneMissing(stderr []byte) bool— case-insensitive substring matcher for tmux's missing-pane stderr ("can't find pane" / "no such pane" / pane…not found); shared byValidatePaneand thewindow-nameverbs' exit-code mapping (tmuxExitCodeinpane_window_name.gois unified onto it)ValidatePane(paneID, server string) error— a single targeted probetmux display-message -t <pane> -p '#{pane_id}', comparing the trimmed output to the argument (ID-exact: window-name / target-grammar args resolve to a different pane ID and are rejected). Version-robust via two detection branches: on tmux ≥3.6 a missing pane exits 0 with empty output (caught by the output==arg comparison — the load-bearing check, verified empirically on 3.6a); older tmux errors with "can't find pane" stderr (caught byIsPaneMissing). Missing pane → the typed*PaneNotFoundError(messagepane <id> not found, byte-identical to the historical string; detectable viaerrors.As, which is how the validated verbs map it to exit 2 vs. 3 — no string matching); other tmux failures (dead server, bad socket) surface stderr viaStderrError. Pure decision half extracted asvalidatePaneResultfor tmux-free testsGetPanePID(paneID, server string) (int, error)— resolves shell PID viatmux display-messageCurrentCommand(server, paneID string) (string, error)(+ itsCurrentCommandArgsargv builder) — reads a pane's foreground command viatmux display-message -p -t <pane> '#{pane_current_command}', trimmed; server-first,RunCmd+StderrError, matching theCapture/SendLiteralconventions. It backs the readiness gate's agent-takeover precondition and is the same foreground-command signal the operator'sagent_exiteddelta keys onIsShellCommand(cmd string) bool— the shared shell-name predicate: case-sensitive basename match against the fixed nine-name setsh bash zsh fish dash ksh tcsh csh nu, empty string never matches. One home for both pane-foreground consumers — the gate's takeover precondition and the operator'sagent_exiteddeltaReadWindowName(paneID, server string) (string, []byte, error)— reads the tmux window name viatmux display-message -p -t <pane> '#W', trimmed; delegates toRunCmd. Returns (name, tmux stderr bytes, exec error) — callers use the stderr bytes to map tmux's "can't find pane" message to exit 2 vs. other tmux failures to exit 3. Used by thewindow-namesubcommand group.ResolvePaneContext(paneID, mainRoot, server string) (*PaneContext, error)— resolves worktree, change, stage, and agent state from the pane's CWDFindMainWorktreeRoot(cwds []string) string— derives the main worktree root from pane CWDs viagit worktree list --porcelainWithServer(server string, args ...string) []string— the canonical argv-building helper (see Design Decisions)- The pre-send pane-mode guard:
SendLiteral/SendKeyfirst runensureNoMode(server, paneID)— probe#{pane_in_mode}(InModeArgsbuilder;paneInModepure decision, trimmed output== "1") and, only when set, exit the mode viasend-keys -X cancel(CancelModeArgs) before sending. Keys sent into a pane in a tmux mode (copy-mode from a human scrolling up, choose-tree, …) are consumed as mode key bindings —send-keysexits 0 and nothing reaches the application — so every Go send path (the gate choreography,fab pane deliver/ready,fab dispatch deliver) clears the mode first. The cancel is conditional because-X cancelerrors against a pane not in a mode; probe failures surface viaStderrErrornaming the pane. Mode commands (send-keys -X) are not blocked by a read-only tmux client, so the guard still works in environments where ordinary sends are blocked - The readiness gate and verified-delivery choreography (
gate.go+gate_rk.go):Gate/NewGate,Probe(two-arm — past the shell-foreground takeover precondition, the rk arm ingate_rk.godelegates classification to a sentinel-capablerk mux await --ready, fail-open with warn-once to the raw-tmux arm; the mapping table and contract live in dispatch.md §fab dispatch ready),Deliver,DeriveReadiness(the raw arm's pure classifier), theReadinessconstants (ReadyReady/ReadyBooting/ReadyParked— the exact report stringsfab pane readyandfab dispatch readyprint),ReadySentinel,SnippetLines,Snippet,PaneIO, the wrap-tolerant echo counter (countWrapped/squeeze, dropping whitespace and U+2500–U+257F box-drawing runes), andTail.gate_rk.goowns the delegation seams: theexec.LookPathgate, the sentinel-capability probe (rk mux await --helpmentioningparked, cached per process), therkAwaitArgsargv builder with the unexportedrkReadyTimeoutconstant, and the injectable package-level runner vars (rkAwaitRunner/rkSentinelProbe— therkPanesRunnerprecedent). One copy of the classifier and choreography serves both thefab paneprimitives and thefab dispatchbindings - The pane creators and lifecycle mechanics (
create.go):OpenWindow,OpenSplitPane,OpenPlainPane(the unsized, untitled split/window behindfab pane open),SplitPlacement(+Describe) with its package-scopesplitArgsargv renderer,ServerReachable,PaneAlive,KillPane, andPointerPrompt(theRead <path> and execute it.pointer-line composerfab pane deliver --prompt-fileandfab dispatch delivershare). The placement decision half (SplitTarget/SelectPaneShape/SiblingDispatchPane) stays ininternal/dispatch— it reads dispatch records and config
All tmux-invoking functions accept a trailing server string parameter and build their argv via WithServer. Callers in cmd/fab/pane*.go read the flag via cmd.Flags().GetString("server") and thread the value through. The RunCmd/StderrError pair is applied at the capture (capturePaneContent), operator (tmux new-window, gitRepoRoot), batch-new (wt create, tmux new-window), and batch-switch (wt create — added by 260717-otol when batch switch moved off .Output() to surface wt's typed exit-2 error in its warn-and-skip line) subprocess sites — errors include the trimmed child stderr and the relevant identifier (pane ID / target).
Design Decisions
Map Enumeration Delegated to rk mux panes, Fail-Open In-Binary
Decision: fab pane map sources its enumeration from rk mux panes --json when rk is present — an in-binary exec (capability-probed via exec.LookPath, silent fallback to the internal tmux list-panes path on any failure) — keeping fab as the change/stage enrichment layer per cli-layering's two-layer model. Session scoping is applied as a fab-side row filter over rk's whole-server view.
Why: rk owns the tmux substrate; its enumeration is strictly richer (reconciled agent state, internal-session exclusion, pinned-window dedup) and duplicate enumeration is the drift liability the layering plan removes. In-binary delegation keeps the operator hot path a single command, matching how consumers already invoke map.
Rejected: A new stdin-filter verb / skill-level piping (the change/stage join is Go logic inside fab; two commands on the hot path); requiring rk (fab must work rk-less, delegation rule 2); reimplementing rk's session filtering on the fallback path (rule 1 cuts both ways — the fallback keeps the raw view).
Introduced by: 260820-89vn-pane-map-rk-enumeration
Structured Agent-State Pair Replaces the Raw-Option Thread in Map Rows
Decision: paneEntry/paneRow carry a resolved (agentState, agentIdleDur) pair instead of the raw @rk_pane_agent_state string; the internal path resolves the pair at parse time via pane.AgentDisplayFromOption, the delegated path fills it from rk's JSON (dropping a waiting duration to keep agent_idle_duration idle-only).
Why: rk rows carry no raw option — reconciled state + formatted duration only — so the raw string cannot remain the shared row representation; resolving once at discovery gives both paths one downstream render/JSON seam.
Rejected: Synthesizing a fake state:epoch value from rk rows (cannot reconstruct an epoch from a formatted duration); dual-field rows with a source discriminator (two render paths to keep in sync); surfacing rk's waiting duration (changes the published JSON contract).
Introduced by: 260820-89vn-pane-map-rk-enumeration
The Snapshot Row Carries command Internally, Never as Output
Decision: The pane-map/tick snapshot row carries the pane's current foreground command as a command field on BOTH enumeration paths — the rk row's own command on the delegated path, and #{pane_current_command} appended as the ninth tab-separated field of the internal list-panes -F format (window_id stays field 8; legacy 5–8-field lines still parse with command == ""). The field is snapshot-internal: fab pane map's human table and --json output carry no new column or key, and its sole consumer is the operator tick's shell-branch trigger for agent_exited confirmation (see operator.md).
Why: The tick needs a cheap provider-agnostic trigger for the rare process-tree confirmation, and pane_current_command is an input both enumeration paths already carry; keeping it off the rendered output preserves rk-contract parity while nothing consumes it externally.
Rejected: A new fab pane map column or JSON key (output surface for an internal predicate input, against the delegation parity rule); re-deriving the signal per tick from a separate display-message probe (extra subprocess on the hot path).
Introduced by: 260829-1xqx-agent-exit-shell-fallback
Agent Classification Is Config-Derived and Bounded
Decision: Process classification accepts an explicit agent-name set. Command callers derive it from claude/claude-code plus merged providers' quote-aware interactive_command leading-word basenames, exclude shells, and pass it through process discovery. Matching checks comm first and only the first two cmdline token basenames; the tree scan includes its root.
Why: One classifier keeps fab pane process output and the operator's liveness confirmation consistent across built-in and project providers, while the two-token window recognizes interpreter wrappers without treating prompt text as a running agent.
Rejected: A global classifier tied to config state; Claude-only names; comm-only matching; whole-cmdline substring scans; deriving agent names from headless_command, whose leading shell wrappers do not identify the interactive pane process.
Introduced by: 260902-ssyf-operator-liveness-process-tree
Unknown --session Name Yields an Empty Result on the Delegated Path
Decision: --session bogus with rk present filters to zero rows and prints No tmux panes found. (exit 0); the fallback path keeps tmux's own error for an unknown -t target.
Why: the filter model has no "session exists but is empty vs. doesn't exist" probe without an extra subprocess on the operator hot path; the empty-result answer is honest under the whole-server view.
Rejected: a tmux has-session pre-check per invocation (extra subprocess to preserve an error message).
Introduced by: 260820-89vn-pane-map-rk-enumeration
Capture Tail Lives in the Shared pane.Capture, Not the CLI Layer
Decision: The last-N tail (pane.TailLines: strip trailing blank screen-padding, take the last N lines, bytes within the window untouched) is applied inside internal/pane.Capture, so all three capture consumers — fab pane capture, the dispatch readiness probe, and the delivery choreography (gate.go) — share one corrected "last N lines" semantic. The tmux argv is unchanged: -S -N remains the raw-material fetch (it guarantees ≥N lines of material whenever the pane has that much history), and the tail is pure post-processing. TailLines is deliberately distinct from gate.go's Tail (the []byte log-file tailer with no padding strip) — the two are cross-referenced in comments and must not be consolidated.
Why: -S -N sets only the capture-window START, so the raw output is N scrollback lines plus the entire visible screen — -l 5 used to return ~45 lines, pushing operators to | tail -N or raw tmux. internal/pane pins builder+runner together precisely so consumers cannot drift on the capture range; gate comparisons are transform-symmetric (both sides of every before/after check pass through the same tail), so the gate needed no code change.
Rejected: Tailing only in cmd/fab/pane_capture.go — keeps the gate byte-identical but forks capture semantics across consumers, re-introducing exactly the drift the shared package exists to prevent.
Introduced by: 260819-y4mu-pane-capture-tail-last-lines
Targeted display-message Probe with Two Detection Branches (ValidatePane)
Decision: ValidatePane is a single tmux display-message -t <pane> -p '#{pane_id}' probe whose trimmed output must equal the argument, with two detection branches for a missing pane: the output==arg comparison (load-bearing on tmux ≥3.6, where display-message exits 0 with empty output for a missing pane) and the IsPaneMissing stderr mapping (older tmux, which errors with "can't find pane").
Why: The previous tmux list-panes -a pre-check enumerated every pane on the server before each capture/process invocation and was TOCTOU-ineffective anyway. The probe keeps both contracts the enumeration provided — existence checking and ID-exactness (-t alone accepts the full tmux target grammar: window names, session:win.pane — a behavioral loosening) — at O(1) subprocess cost. Empirical verification on tmux 3.6a contradicted the originally assumed stderr-only error path, so both branches are required for version robustness; error-path equivalence (at the time: missing pane → Error: pane <id> not found exit 1; dead server → exit 1 with stderr detail now included) was re-verified before the old path was removed. (The flat exit-1 codes described here were subsequently split into the pane-family 2/3 scheme by 260612-ye8r — see the Error behavior sections above.)
Rejected: Bare -t targeting without the output comparison (accepts the full target grammar — loosens ID-exactness). Keeping the list-panes -a pre-check (O(server) per invocation, race-prone). New helpers in a fresh internal/tmuxutil package (over-engineering for three helpers; internal/pane is the documented home for cross-package tmux helpers per the WithServer decision below).
Introduced by: 260612-pw3k-operator-pane-perf-error-surfacing
Persistent Flag on the Parent, Not Per-Subcommand
Decision: --server is registered as a persistent flag on paneCmd via cmd.PersistentFlags().StringP("server", "L", "", "..."), visible on all eight subcommands. Each subcommand reads the value via cmd.Flags().GetString("server").
Why: Cobra idiom for a flag that applies uniformly across a command group. Single registration point, single help-text location, zero chance of per-subcommand drift.
Rejected: Per-subcommand registration — one copy of the same flag per subcommand, as many places to update if the description changes.
Introduced by: 260417-2fbb-pane-server-flag
WithServer Helper in internal/pane/pane.go
Decision: A single argv-building helper WithServer(server string, args ...string) []string lives in src/go/fab/internal/pane/pane.go. It returns args unchanged when server == "" and append([]string{"-L", server}, args...) otherwise. Every exec.Command("tmux", ...) site in the pane call tree builds its argv via this helper.
Why: WithServer is a short pure function that eliminates per-file conditional logic and ensures the -L prepend is identical at every call site. Scope is exactly one helper for one flag; introducing an internal/tmuxutil/ package or a TmuxClient struct type would be over-engineering for a single-flag change and can be promoted later if tmux-helper surface grows.
Exported (rather than unexported as drafted in the spec): the helper is used from the cmd/fab package (e.g., inside listPanesArgs, capturePaneArgs) to keep a single canonical argv builder across packages. Cross-package argv builders in this codebase are exported from internal/pane when consumed outside the pane package — future tmux-helper additions should follow the same pattern.
Introduced by: 260417-2fbb-pane-server-flag
Helper Named WithServer, Not tmuxArgs
Decision: The helper is named WithServer.
Why: At introduction, tmuxArgs was already a local variable name in a pane command file; a free function of the same name would shadow or collide, and renaming the local variable creates churn outside the flag's scope. WithServer also reads naturally at call sites: exec.Command("tmux", WithServer(server, "list-panes", "-a")...).
Introduced by: 260417-2fbb-pane-server-flag
Pass the Server Name Verbatim to Tmux
Decision: The --server value is passed to tmux without fab-side validation, escaping, or normalization.
Why: Tmux owns the semantics of socket labels. Any pre-validation in fab (e.g., tmux -L <server> has-session) would duplicate tmux's own error handling and introduce race conditions (socket created/destroyed between check and use). Propagating tmux's native error is simpler and more accurate.
Rejected: Pre-check via tmux has-session — extra subprocess, and fab would still need to handle the real tmux error from the actual command anyway.
Introduced by: 260417-2fbb-pane-server-flag
-L <name> Only — No -S <path> in First Cut
Decision: Only --server <name> (maps to tmux -L <name>) is exposed. A --socket-path / -S equivalent is a non-goal for the first cut.
Why: -L covers the motivating run-kit case and every named-socket scenario. Callers that truly need a full path rather than a label are rare; adding -S later is cheap and non-breaking.
Rejected: Env-var alternative (FAB_TMUX_SERVER) — adds hidden env coupling; CLI flag is more discoverable via --help and easier to plumb through subprocess-style callers that already build argv slices.
Introduced by: 260417-2fbb-pane-server-flag
Provider-Generic Primitives in the Pane Family, Dispatch as the Record-Keeping Binding
Decision: The readiness classifier, the verified-delivery choreography, the tmux pane creators/liveness/kill helpers, Tail, and PointerPrompt live in internal/pane and are exposed as the record-free fab pane open/ready/deliver verbs; fab dispatch open/ready/deliver are thin bindings over them that add only dispatch-record bookkeeping. Placement policy and all record state stay in internal/dispatch.
Why: Provider probes, warm-ups, and operator interactions need the gate and verified delivery addressed by pane id with no active change — before this split every piece of that choreography was reachable only through a change+stage dispatch record, so probes were hand-rolled with raw tmux. The import graph allows the move cleanly (internal/pane has no dispatch dependency and the gate needs none), and one home for the tmux mechanics means the echo-verify cannot grow a divergent second copy.
Rejected: Ad-hoc --no-record/--pane-id flags on the dispatch verbs (muddies the five-state dispatch contract); a new sibling package (no import cycle exists to force one); worker-column placement on fab pane open (placement is pipeline policy and stays dispatch-owned — the primitive does a plain split).
Introduced by: 260810-1lah-provider-generic-pane-verbs
fab pane open Pins the Provider and Applies the Standard Fill Precedence
Decision: --provider pins the provider at the top of the standard fill precedence (agent.ResolveRoleWith with the provider override set); --role selects whose {model}/{effort} fills apply, defaulting to default.
Why: A probe spawn wants the same resolved command a pipeline worker would get; fab agent --provider's deliberate fill bypass composes a profile-free invocation the probe would then have to hand-tune.
Rejected: Reusing fab agent --provider's bypass semantics (contradicts the probe use case).
Introduced by: 260810-1lah-provider-generic-pane-verbs
Capture/Process/Kill Demoted to Dispatch-Internal, Skill-Facing Use on rk's Twins
Decision: fab pane capture, fab pane process, and fab pane kill stay as CLI verbs — behavior, flags, exit codes, and help visibility unchanged (no cobra hiding) — but are documented as dispatch-internal: skill-facing raw peek / process-tree inspection / pane removal rides run-kit's substrate twins rk mux capture/process/kill (command -v rk-gated), with the rk-absent skill-facing fallback being raw tmux (capture-pane/kill-pane) plus fab pane map for agent state — never the dispatch-internal fab pane verbs. Scoped to raw peek specifically: fab pane questions is the named exception — a policy-bearing sweep (fab-operator's question-detection guards and indicator patterns mechanized per candidate pane), not a peek primitive, so it lives skill-facing in fab pane on purpose rather than rk mux.
Why: cli-layering's two-layer model gives rk the pane substrate; the rk twins are also strictly richer for skill-facing use (rk mux kill is agent-state-gated, rk mux process classifies the agent from instrumentation, rk mux capture reports reconciled state). Unlike the retired send/await, these verbs are genuinely invoked by the dispatch orchestrator's rk-less pane arm (_preamble.md § CLI-Adapter Dispatch peek, fab dispatch logs' suggested command), so deletion would break a supported rk-less path; rule 2 makes rk optional. The raw-tmux skill-facing fallback (not fab pane capture) is what actually drops the verbs from skill-facing guidance — naming them as fallback would keep the duplicate-guidance drift alive.
Rejected: Deleting the verbs (the pane arm must work rk-less); hiding them in cobra (a CLI surface change with tests + standards audit for zero layering gain — the demotion mechanism is guidance-level); fab pane capture as the rk-absent skill-facing fallback (contradicts "dropped from skill-facing guidance"); pushing questions to rk mux (the sweep encodes fab-operator's own detection policy, not a generic substrate primitive).
Introduced by: 260820-4un7-guidance-repoint-rk-mux-twins (scoped to raw peek with the questions exception named by 260823-dckc-pane-questions-sweep)
Pre-Send Pane-Mode Guard at the SendLiteral/SendKey Seam
Decision: The #{pane_in_mode} probe + conditional send-keys -X cancel guard runs inside SendLiteral/SendKey (via the shared ensureNoMode helper), not per-choreography and not inside runSend.
Why: All Go senders funnel through these two exported functions (gate.go's tmuxPaneIO delegates to them; no other send-keys exists in the Go tree), so every current and future sender inherits the guard for free; runSend receives a prebuilt argv without the server string, so the guard sits one level up where server/paneID are in hand. Per-send guarding also covers a human re-entering copy-mode between a choreography's keystrokes. The guard is transparent (no output on cancel) — delivery's capture-verify remains the observable correctness check. Captures deliberately carry no guard: capture-pane reads the live grid during copy-mode, and auto-cancelling on capture would yank a human out of scrollback on every peek.
Rejected: Guarding inside Gate (misses non-gate senders); probing once per delivery (races mid-choreography mode entry); a capture-side guard (empirically unnecessary and human-hostile); a stderr warning on cancel (noise in choreography retries).
Introduced by: 260902-13qo-pane-send-copy-mode-guard
Agent Messaging Verbs Retired to run-kit's rk mux
Decision: fab pane send and fab pane await are retired as CLI verbs; agent messaging (gated send, record-free await) rides run-kit's rk mux send / rk mux await — every use command -v rk-gated and fail-open to raw tmux send-keys plus the manual delivery probe / poll-capture await when rk is absent (never an error). The gate matrix (--answer permits waiting, refuses active; unknown warns-and-sends; --force skips; --key covers key-name input) and the await contract (--until/--file/--timeout; report gone = exit 1) are rk-owned — the run-kit repo's memory (agent-messaging.md) carries the full contract; do not restate it here. internal/pane's send builders (SendLiteral/SendKey and their argv builders) and the gate/deliver choreography stay — they back fab pane deliver / fab dispatch deliver; fab dispatch wait keeps its own record-keyed loop. The orphaned record-free Await control loop (internal/pane/await.go, whose only non-test consumer was the deleted verb) went with it.
Why: rk owns the tmux substrate (the @rk_pane_agent_state convention, pane interaction verbs); fab owns choreography. Two implementations of one gate matrix is a standing version-skew and maintenance liability, and rk's sender is strictly stronger — probe-verified delivery (baseline capture → bracketed paste → novelty echo probe → probe-gated Enter), --key, stdin multi-line paste, and --await ask-and-wait composition.
Rejected: Keeping fab-side aliases or a fab-binary fallback for the retired verbs (defeats the retirement; two gate implementations is the liability being removed — the rk-absent path degrades to the caller's own state read + raw tmux, never to a fab-binary gate).
Introduced by: 260815-4i0n-retire-pane-send-await