Created
January 16, 2026 23:33
-
-
Save jimmc414/c45d5e54b540d5782560ec99e884142a to your computer and use it in GitHub Desktop.
Fork Claude agents into observable tmux panes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Spec: tmux Agent Panes | |
| > Fork Claude agents into observable tmux panes | |
| ## Overview | |
| This skill enables Claude to spawn parallel agents, each working in their own tmux pane. The user can observe all agents working simultaneously in a single terminal window. | |
| ``` | |
| ┌─────────────────────┬─────────────────────┬─────────────────────┐ | |
| │ Agent 1: Tests │ Agent 2: Linting │ Agent 3: Build │ | |
| │ │ │ │ | |
| │ $ pytest -v │ $ ruff check . │ $ npm run build │ | |
| │ PASSED test_foo │ Fixed 3 issues │ Building... │ | |
| │ PASSED test_bar │ All clean! │ Done! │ | |
| │ │ │ │ | |
| └─────────────────────┴─────────────────────┴─────────────────────┘ | |
| User watches all three agents work in real-time | |
| ``` | |
| ## Core Concepts | |
| ### 1. Session Architecture | |
| ``` | |
| claude-agents (tmux session) | |
| ├── window 0: "orchestrator" (optional - for status) | |
| ├── window 1: "workers" | |
| │ ├── pane 0: agent-1 (e.g., running tests) | |
| │ ├── pane 1: agent-2 (e.g., linting) | |
| │ └── pane 2: agent-3 (e.g., building) | |
| ``` | |
| ### 2. Agent Isolation | |
| Each agent: | |
| - Runs in its own tmux pane | |
| - Has independent bash context | |
| - Can be captured independently | |
| - Works on assigned task without interference | |
| ### 3. Orchestrator Role | |
| The main Claude instance: | |
| - Creates/manages the tmux session | |
| - Spawns panes for each agent task | |
| - Monitors progress via `capture-pane` | |
| - Aggregates results when complete | |
| ## tmux Commands Reference | |
| ### Session Management | |
| ```bash | |
| # Create session | |
| tmux new-session -d -s claude-agents | |
| # Kill session when done | |
| tmux kill-session -t claude-agents | |
| ``` | |
| ### Pane Management | |
| ```bash | |
| # Split horizontally (side by side) | |
| tmux split-window -h -t claude-agents | |
| # Split vertically (stacked) | |
| tmux split-window -v -t claude-agents | |
| # Create 3 equal columns | |
| tmux select-layout -t claude-agents even-horizontal | |
| # Create grid (2x2) | |
| tmux select-layout -t claude-agents tiled | |
| ``` | |
| ### Targeting Specific Panes | |
| ```bash | |
| # Pane addressing: session:window.pane | |
| tmux send-keys -t claude-agents:0.0 "echo agent 1" Enter | |
| tmux send-keys -t claude-agents:0.1 "echo agent 2" Enter | |
| tmux send-keys -t claude-agents:0.2 "echo agent 3" Enter | |
| # Capture specific pane output | |
| tmux capture-pane -t claude-agents:0.0 -p | |
| tmux capture-pane -t claude-agents:0.1 -p | |
| ``` | |
| ### Pane Identification | |
| ```bash | |
| # List all panes with IDs | |
| tmux list-panes -t claude-agents -F "#{pane_index}: #{pane_title}" | |
| # Set pane title (visible in status) | |
| tmux select-pane -t claude-agents:0.0 -T "Tests" | |
| tmux select-pane -t claude-agents:0.1 -T "Lint" | |
| ``` | |
| ## Skill Invocation | |
| ### Skill Name | |
| `/tmux-agents` or `/parallel-panes` | |
| ### Arguments | |
| ``` | |
| /tmux-agents spawn <name> "<command>" # Create new agent pane | |
| /tmux-agents list # Show all agent panes | |
| /tmux-agents capture <name> # Get output from pane | |
| /tmux-agents cleanup # Kill session | |
| ``` | |
| ### Example Usage | |
| ``` | |
| User: Run tests, lint, and build in parallel so I can watch | |
| Claude: I'll spawn three agents in separate panes. | |
| /tmux-agents spawn tests "pytest -v" | |
| /tmux-agents spawn lint "ruff check ." | |
| /tmux-agents spawn build "npm run build" | |
| A window should open showing all three. Let me check their progress... | |
| /tmux-agents capture tests | |
| → 15 passed, 1 failed | |
| /tmux-agents capture lint | |
| → All checks passed | |
| /tmux-agents capture build | |
| → Build complete | |
| ``` | |
| ## Implementation | |
| ### Phase 1: Session Setup | |
| ```bash | |
| #!/bin/bash | |
| # tmux_agents_setup.sh | |
| SESSION="claude-agents" | |
| # Kill existing if any | |
| tmux kill-session -t $SESSION 2>/dev/null | |
| # Create new session | |
| tmux new-session -d -s $SESSION | |
| # Open observer window (WSL) | |
| if [[ -d "/mnt/c" ]]; then | |
| /mnt/c/Users/*/AppData/Local/Microsoft/WindowsApps/wt.exe \ | |
| wsl tmux attach -t $SESSION & | |
| fi | |
| ``` | |
| ### Phase 2: Spawn Agent Pane | |
| ```bash | |
| #!/bin/bash | |
| # tmux_agents_spawn.sh <name> <command> | |
| SESSION="claude-agents" | |
| NAME=$1 | |
| CMD=$2 | |
| # Count existing panes | |
| PANE_COUNT=$(tmux list-panes -t $SESSION | wc -l) | |
| # If more than 1 pane, split from last pane | |
| if [ $PANE_COUNT -gt 0 ]; then | |
| tmux split-window -h -t $SESSION | |
| tmux select-layout -t $SESSION even-horizontal | |
| fi | |
| # Get the new pane index | |
| NEW_PANE=$((PANE_COUNT)) | |
| # Set title and run command | |
| tmux select-pane -t $SESSION:0.$NEW_PANE -T "$NAME" | |
| tmux send-keys -t $SESSION:0.$NEW_PANE "# Agent: $NAME" Enter | |
| tmux send-keys -t $SESSION:0.$NEW_PANE "$CMD" Enter | |
| echo "Spawned agent '$NAME' in pane $NEW_PANE" | |
| ``` | |
| ### Phase 3: Capture Output | |
| ```bash | |
| #!/bin/bash | |
| # tmux_agents_capture.sh <pane_index> | |
| SESSION="claude-agents" | |
| PANE=$1 | |
| tmux capture-pane -t $SESSION:0.$PANE -p | |
| ``` | |
| ### Phase 4: Cleanup | |
| ```bash | |
| #!/bin/bash | |
| # tmux_agents_cleanup.sh | |
| tmux kill-session -t claude-agents 2>/dev/null | |
| echo "Session cleaned up" | |
| ``` | |
| ## Python Implementation | |
| ```python | |
| """tmux_agent_panes.py - Spawn observable agent panes""" | |
| import subprocess | |
| import os | |
| from dataclasses import dataclass | |
| from typing import Optional | |
| @dataclass | |
| class AgentPane: | |
| name: str | |
| pane_index: int | |
| command: str | |
| class TmuxAgentManager: | |
| SESSION = "claude-agents" | |
| def __init__(self): | |
| self.agents: dict[str, AgentPane] = {} | |
| self.pane_counter = 0 | |
| def setup(self) -> str: | |
| """Create session and open observer window.""" | |
| # Kill existing | |
| subprocess.run( | |
| ["tmux", "kill-session", "-t", self.SESSION], | |
| capture_output=True | |
| ) | |
| # Create new | |
| subprocess.run( | |
| ["tmux", "new-session", "-d", "-s", self.SESSION] | |
| ) | |
| # Open observer (WSL) | |
| if os.path.exists("/mnt/c"): | |
| wt_path = next( | |
| (f"/mnt/c/Users/{u}/AppData/Local/Microsoft/WindowsApps/wt.exe" | |
| for u in os.listdir("/mnt/c/Users") | |
| if os.path.exists(f"/mnt/c/Users/{u}/AppData/Local/Microsoft/WindowsApps/wt.exe")), | |
| None | |
| ) | |
| if wt_path: | |
| subprocess.Popen( | |
| [wt_path, "wsl", "tmux", "attach", "-t", self.SESSION], | |
| stdout=subprocess.DEVNULL, | |
| stderr=subprocess.DEVNULL | |
| ) | |
| return f"Session '{self.SESSION}' created. Observer window opened." | |
| def spawn(self, name: str, command: str) -> str: | |
| """Spawn a new agent in its own pane.""" | |
| if self.pane_counter > 0: | |
| # Split to create new pane | |
| subprocess.run([ | |
| "tmux", "split-window", "-h", "-t", self.SESSION | |
| ]) | |
| # Rebalance layout | |
| subprocess.run([ | |
| "tmux", "select-layout", "-t", self.SESSION, "even-horizontal" | |
| ]) | |
| pane_id = f"{self.SESSION}:0.{self.pane_counter}" | |
| # Set pane title | |
| subprocess.run([ | |
| "tmux", "select-pane", "-t", pane_id, "-T", name | |
| ]) | |
| # Send command | |
| subprocess.run([ | |
| "tmux", "send-keys", "-t", pane_id, | |
| f"# === Agent: {name} ===", "Enter" | |
| ]) | |
| subprocess.run([ | |
| "tmux", "send-keys", "-t", pane_id, command, "Enter" | |
| ]) | |
| # Track agent | |
| self.agents[name] = AgentPane( | |
| name=name, | |
| pane_index=self.pane_counter, | |
| command=command | |
| ) | |
| self.pane_counter += 1 | |
| return f"Agent '{name}' spawned in pane {self.pane_counter - 1}" | |
| def capture(self, name: str) -> str: | |
| """Capture output from an agent's pane.""" | |
| if name not in self.agents: | |
| return f"Agent '{name}' not found" | |
| pane_id = f"{self.SESSION}:0.{self.agents[name].pane_index}" | |
| result = subprocess.run( | |
| ["tmux", "capture-pane", "-t", pane_id, "-p"], | |
| capture_output=True, | |
| text=True | |
| ) | |
| return result.stdout | |
| def capture_all(self) -> dict[str, str]: | |
| """Capture output from all agents.""" | |
| return {name: self.capture(name) for name in self.agents} | |
| def cleanup(self): | |
| """Kill the session.""" | |
| subprocess.run( | |
| ["tmux", "kill-session", "-t", self.SESSION], | |
| capture_output=True | |
| ) | |
| self.agents.clear() | |
| self.pane_counter = 0 | |
| return "Session cleaned up" | |
| # Singleton for skill usage | |
| _manager: Optional[TmuxAgentManager] = None | |
| def get_manager() -> TmuxAgentManager: | |
| global _manager | |
| if _manager is None: | |
| _manager = TmuxAgentManager() | |
| return _manager | |
| ``` | |
| ## Skill File | |
| ```markdown | |
| # /tmux-agents skill | |
| ## Description | |
| Spawn parallel Claude agents in observable tmux panes. | |
| ## Commands | |
| ### setup | |
| Create the tmux session and open observer window. | |
| ``` | |
| /tmux-agents setup | |
| ``` | |
| ### spawn <name> "<command>" | |
| Create a new agent pane running the given command. | |
| ``` | |
| /tmux-agents spawn tests "pytest -v tests/" | |
| /tmux-agents spawn lint "ruff check src/" | |
| ``` | |
| ### capture <name> | |
| Get the current output from an agent's pane. | |
| ``` | |
| /tmux-agents capture tests | |
| ``` | |
| ### capture-all | |
| Get output from all agent panes. | |
| ``` | |
| /tmux-agents capture-all | |
| ``` | |
| ### cleanup | |
| Kill the session and all agent panes. | |
| ``` | |
| /tmux-agents cleanup | |
| ``` | |
| ## Example Workflow | |
| ``` | |
| User: Run the full CI pipeline and let me watch | |
| Claude: I'll set up parallel agents for each CI step. | |
| /tmux-agents setup | |
| → Session created, observer window opened | |
| /tmux-agents spawn typecheck "npx tsc --noEmit" | |
| /tmux-agents spawn tests "pytest -v" | |
| /tmux-agents spawn lint "ruff check ." | |
| /tmux-agents spawn build "npm run build" | |
| You should see 4 panes now. I'll monitor their progress... | |
| [waits, then captures] | |
| /tmux-agents capture-all | |
| → typecheck: No errors | |
| → tests: 42 passed | |
| → lint: All checks passed | |
| → build: Build successful | |
| All CI steps complete! Cleaning up... | |
| /tmux-agents cleanup | |
| ``` | |
| ## Layout Options | |
| For many agents, use tiled layout: | |
| ```bash | |
| tmux select-layout -t claude-agents tiled | |
| ``` | |
| Layout options: | |
| - `even-horizontal` - side by side columns | |
| - `even-vertical` - stacked rows | |
| - `tiled` - grid pattern | |
| - `main-horizontal` - one big, rest below | |
| - `main-vertical` - one big, rest to the right | |
| ## Integration with Claude's Task Tool | |
| ### The Problem: Invisible Delegation | |
| When Claude uses the Task tool today, sub-agents are invisible: | |
| ``` | |
| You ←→ Claude (main) ──Task──→ [Sub-agent works invisibly] ──→ Result | |
| │ | |
| └── You see NOTHING here | |
| It's a black box | |
| ``` | |
| The sub-agent runs, uses tools (Bash, Read, Write), and returns results. But the user has **zero visibility** into what it's actually doing. | |
| ### The Vision: Observable Delegation | |
| ``` | |
| You ←→ Claude (main) ──Task──→ Sub-agent in Pane 1 ──→ Result | |
| ──Task──→ Sub-agent in Pane 2 ──→ Result | |
| ──Task──→ Sub-agent in Pane 3 ──→ Result | |
| │ | |
| └── You WATCH all of them work | |
| ``` | |
| ### The Challenge | |
| **Task agents don't run in a terminal.** They make API calls. | |
| When a Task agent uses the Bash tool, it's not typing into a shell you can see - it's making an API request that executes a command and returns output. | |
| ``` | |
| Task Agent Reality | |
| ─────────── ─────── | |
| "I'll run pytest" API call to Bash tool | |
| ↓ | |
| subprocess.run(["pytest"]) | |
| ↓ | |
| Returns stdout to agent | |
| User sees: nothing | |
| ``` | |
| ### Two Types of Delegation | |
| | Invisible (current) | Visible (proposed) | | |
| |--------------------|-------------------| | |
| | "Go figure it out" | "Work while I watch" | | |
| | Trust the agent | Verify the agent | | |
| | Results matter | Process matters | | |
| | Background work | Collaborative work | | |
| | Fire and forget | Observe and intervene | | |
| Both are valid use cases. Sometimes you want to delegate and get results. Sometimes you want to watch and potentially intervene. | |
| ### Architecture Options | |
| #### Option 1: Mirror/Log Approach (Easiest) | |
| ``` | |
| Task Agent runs normally (invisible) | |
| │ | |
| └──→ We intercept tool calls | |
| │ | |
| └──→ Mirror to tmux pane (display only) | |
| │ | |
| └──→ User sees what agent is doing | |
| (read-only observation) | |
| ``` | |
| The agent still works invisibly via API, but we **log** its actions to a visible pane. User watches a stream of what's happening. | |
| **Pros:** | |
| - Doesn't change how Task tool works | |
| - Easy to implement | |
| - No performance impact on agent | |
| **Cons:** | |
| - Not truly "live" - slight delay | |
| - User can't intervene | |
| - Read-only observation | |
| **Implementation:** | |
| ```python | |
| class TaskLogger: | |
| """Logs Task agent activity to a tmux pane.""" | |
| def __init__(self, pane_manager: TmuxAgentManager): | |
| self.pane_manager = pane_manager | |
| self.pane_id = None | |
| def start_task(self, name: str, prompt: str): | |
| """Called when Task tool is invoked.""" | |
| self.pane_id = self.pane_manager.spawn(name, "# Task agent starting...") | |
| self._log(f"Task: {name}") | |
| self._log(f"Prompt: {prompt[:80]}...") | |
| self._log(f"Started: {datetime.now()}") | |
| self._log("") | |
| def log_tool_call(self, tool: str, params: dict): | |
| """Called when agent uses a tool.""" | |
| self._log(f"[{tool}] {self._summarize(params)}") | |
| def log_tool_result(self, tool: str, result: str): | |
| """Called when tool returns.""" | |
| self._log(f" → {result[:100]}...") | |
| def end_task(self, result: str): | |
| """Called when Task completes.""" | |
| self._log("") | |
| self._log("=== Task Complete ===") | |
| self._log(f"Result: {result[:200]}...") | |
| def _log(self, msg: str): | |
| subprocess.run([ | |
| "tmux", "send-keys", "-t", self.pane_id, | |
| f"echo '{msg}'", "Enter" | |
| ]) | |
| ``` | |
| #### Option 2: Redirect Bash Through tmux (Medium Difficulty) | |
| ``` | |
| Task Agent wants to run "pytest" | |
| │ | |
| └──→ Instead of subprocess.run() | |
| │ | |
| └──→ tmux send-keys "pytest" | |
| │ | |
| └──→ User sees command run live | |
| │ | |
| └──→ tmux capture-pane gets output | |
| │ | |
| └──→ Returns to agent | |
| ``` | |
| The agent's Bash commands actually execute in the visible tmux pane. Real terminal, real visibility. | |
| **Pros:** | |
| - True live observation | |
| - Real terminal environment | |
| - User sees exactly what happens | |
| **Cons:** | |
| - Requires modifying Bash tool behavior | |
| - Slightly slower (tmux overhead) | |
| - More complex error handling | |
| **Implementation:** | |
| ```python | |
| class ObservableBashTool: | |
| """Bash tool that executes in visible tmux pane.""" | |
| def __init__(self, pane_id: str): | |
| self.pane_id = pane_id | |
| self.marker = "___CMD_DONE___" | |
| def execute(self, command: str, timeout: int = 120) -> str: | |
| """Execute command in tmux pane and capture output.""" | |
| # Clear and send command with end marker | |
| full_cmd = f"{command}; echo '{self.marker}'" | |
| subprocess.run([ | |
| "tmux", "send-keys", "-t", self.pane_id, | |
| full_cmd, "Enter" | |
| ]) | |
| # Poll for completion | |
| start = time.time() | |
| while time.time() - start < timeout: | |
| output = subprocess.run( | |
| ["tmux", "capture-pane", "-t", self.pane_id, "-p"], | |
| capture_output=True, text=True | |
| ).stdout | |
| if self.marker in output: | |
| # Extract output between command and marker | |
| return self._extract_output(output, command) | |
| time.sleep(0.5) | |
| raise TimeoutError(f"Command timed out: {command}") | |
| ``` | |
| #### Option 3: Custom Agents via Agent SDK (Most Control) | |
| Build agents from scratch using Claude Agent SDK, designed to be observable from the ground up: | |
| ```python | |
| from claude_code_sdk import Agent, Tool | |
| import asyncio | |
| class ObservableAgent: | |
| """An agent that does all work in a visible tmux pane.""" | |
| def __init__(self, name: str, pane_manager: TmuxAgentManager): | |
| self.name = name | |
| self.pane_manager = pane_manager | |
| self.pane_id = None | |
| async def run(self, prompt: str) -> str: | |
| """Run agent with all work visible in tmux pane.""" | |
| # Create dedicated pane | |
| self.pane_id = self.pane_manager.spawn( | |
| self.name, | |
| f"echo '=== Agent: {self.name} ==='" | |
| ) | |
| # Create agent with custom Bash tool | |
| agent = Agent( | |
| model="claude-sonnet-4-20250514", | |
| tools=[ | |
| self._observable_bash_tool(), | |
| self._observable_read_tool(), | |
| self._observable_write_tool(), | |
| ] | |
| ) | |
| # Run and return result | |
| result = await agent.run(prompt) | |
| self._log("=== Agent Complete ===") | |
| return result | |
| def _observable_bash_tool(self) -> Tool: | |
| """Bash tool that executes in our pane.""" | |
| def execute(command: str) -> str: | |
| self._send(command) | |
| return self._capture_after(command) | |
| return Tool( | |
| name="bash", | |
| description="Execute shell commands", | |
| function=execute | |
| ) | |
| def _send(self, cmd: str): | |
| subprocess.run([ | |
| "tmux", "send-keys", "-t", self.pane_id, | |
| cmd, "Enter" | |
| ]) | |
| def _capture_after(self, cmd: str, timeout: int = 60) -> str: | |
| # Wait for prompt to return, then capture | |
| time.sleep(1) # Simple approach | |
| return subprocess.run( | |
| ["tmux", "capture-pane", "-t", self.pane_id, "-p"], | |
| capture_output=True, text=True | |
| ).stdout | |
| ``` | |
| #### Option 4: Hybrid Workers (Pragmatic) | |
| For tasks that are primarily terminal work (tests, builds, linting), skip the Task tool entirely and just use tmux panes with shell commands: | |
| ```python | |
| async def parallel_ci_pipeline(pane_manager: TmuxAgentManager): | |
| """Run CI steps as parallel shell jobs, not AI agents.""" | |
| pane_manager.setup() | |
| # These are just shell commands, not AI agents | |
| jobs = [ | |
| ("tests", "pytest -v && echo DONE"), | |
| ("lint", "ruff check . && echo DONE"), | |
| ("typecheck", "mypy . && echo DONE"), | |
| ("build", "npm run build && echo DONE"), | |
| ] | |
| for name, cmd in jobs: | |
| pane_manager.spawn(name, cmd) | |
| # Monitor completion | |
| while True: | |
| outputs = pane_manager.capture_all() | |
| if all("DONE" in out for out in outputs.values()): | |
| break | |
| await asyncio.sleep(2) | |
| return outputs | |
| ``` | |
| **When to use this:** | |
| - Tasks are well-defined shell commands | |
| - No AI reasoning needed during execution | |
| - Just need parallelism and visibility | |
| ### Implementation Phases | |
| #### Phase 1: Task Lifecycle Logging (MVP) | |
| Minimal visibility - just show when tasks start/end: | |
| ```python | |
| def observable_task(name: str, prompt: str, pane_manager): | |
| """Wrap Task tool with basic visibility.""" | |
| # Create pane showing task info | |
| pane_id = pane_manager.spawn(name, "# Task starting...") | |
| pane_manager.send(pane_id, f"echo 'Task: {name}'") | |
| pane_manager.send(pane_id, f"echo 'Started: $(date)'") | |
| pane_manager.send(pane_id, f"echo 'Status: Running...'") | |
| # Run actual task (invisible) | |
| result = Task(prompt) # Standard Task tool | |
| # Show completion | |
| pane_manager.send(pane_id, f"echo ''") | |
| pane_manager.send(pane_id, f"echo 'Status: Complete'") | |
| pane_manager.send(pane_id, f"echo 'Finished: $(date)'") | |
| return result | |
| ``` | |
| **User sees:** | |
| ``` | |
| ┌─────────────────────┬─────────────────────┐ | |
| │ Task: research │ Task: implement │ | |
| │ Started: 15:30:00 │ Started: 15:30:00 │ | |
| │ Status: Running... │ Status: Complete │ | |
| │ │ Finished: 15:32:15 │ | |
| └─────────────────────┴─────────────────────┘ | |
| ``` | |
| #### Phase 2: Tool Call Streaming | |
| Show what tools the agent is using: | |
| ```python | |
| def observable_task_v2(name: str, prompt: str, pane_manager): | |
| """Task with tool call visibility.""" | |
| pane_id = pane_manager.spawn(name, "# Task starting...") | |
| # Hook into tool calls (requires Task tool modification) | |
| def on_tool_call(tool: str, params: dict): | |
| summary = f"[{tool}] {summarize(params)}" | |
| pane_manager.send(pane_id, f"echo '{summary}'") | |
| def on_tool_result(tool: str, result: str): | |
| pane_manager.send(pane_id, f"echo ' → {result[:60]}...'") | |
| result = Task( | |
| prompt, | |
| on_tool_call=on_tool_call, | |
| on_tool_result=on_tool_result | |
| ) | |
| return result | |
| ``` | |
| **User sees:** | |
| ``` | |
| ┌─────────────────────────────────────────────────────────────┐ | |
| │ Task: research-auth │ | |
| │ Started: 15:30:00 │ | |
| │ │ | |
| │ [Grep] pattern="auth" path="src/" │ | |
| │ → Found 12 matches │ | |
| │ [Read] file="src/auth/login.py" │ | |
| │ → Read 245 lines │ | |
| │ [Read] file="src/auth/session.py" │ | |
| │ → Read 189 lines │ | |
| │ [Bash] command="grep -r 'JWT' ." │ | |
| │ → Found 3 matches in config │ | |
| │ │ | |
| │ Status: Running... │ | |
| └─────────────────────────────────────────────────────────────┘ | |
| ``` | |
| #### Phase 3: Bash Redirection | |
| Agent's Bash commands execute in visible pane: | |
| ```python | |
| def observable_task_v3(name: str, prompt: str, pane_manager): | |
| """Task with Bash commands visible in pane.""" | |
| pane_id = pane_manager.spawn(name, "# Task starting...") | |
| # Custom Bash tool that uses tmux | |
| observable_bash = ObservableBashTool(pane_id) | |
| result = Task( | |
| prompt, | |
| tool_overrides={ | |
| "bash": observable_bash.execute | |
| } | |
| ) | |
| return result | |
| ``` | |
| **User sees:** | |
| ``` | |
| ┌─────────────────────────────────────────────────────────────┐ | |
| │ # Task: run-tests │ | |
| │ $ pytest -v tests/ │ | |
| │ ========================= test session starts ==============│ | |
| │ collected 42 items │ | |
| │ │ | |
| │ tests/test_auth.py::test_login PASSED │ | |
| │ tests/test_auth.py::test_logout PASSED │ | |
| │ tests/test_api.py::test_endpoint PASSED │ | |
| │ ... │ | |
| │ │ | |
| │ ========================= 42 passed in 3.21s ===============│ | |
| │ $ │ | |
| └─────────────────────────────────────────────────────────────┘ | |
| ``` | |
| #### Phase 4: Full Agent SDK Integration | |
| Custom agents built for observability: | |
| ```python | |
| async def run_observable_agents(tasks: list[dict], pane_manager): | |
| """Run multiple observable agents in parallel.""" | |
| pane_manager.setup() | |
| async def run_one(task: dict): | |
| agent = ObservableAgent(task["name"], pane_manager) | |
| return await agent.run(task["prompt"]) | |
| # Run all in parallel | |
| results = await asyncio.gather(*[ | |
| run_one(task) for task in tasks | |
| ]) | |
| return dict(zip([t["name"] for t in tasks], results)) | |
| # Usage | |
| results = await run_observable_agents([ | |
| {"name": "research", "prompt": "Find all auth-related code"}, | |
| {"name": "tests", "prompt": "Run and fix failing tests"}, | |
| {"name": "docs", "prompt": "Update README with new features"}, | |
| ], pane_manager) | |
| ``` | |
| ### Example: Full Observable Workflow | |
| ``` | |
| User: Refactor the auth module, run tests, and update docs - let me watch | |
| Claude: I'll spawn three observable agents. | |
| /tmux-agents setup | |
| → Session created, observer window opened | |
| # User now sees: | |
| ┌─────────────────────┬─────────────────────┬─────────────────────┐ | |
| │ Agent: Refactor │ Agent: Tests │ Agent: Docs │ | |
| │ │ │ │ | |
| │ [Read] auth.py │ $ pytest -v │ [Read] README.md │ | |
| │ → 245 lines │ test_login PASSED │ → 120 lines │ | |
| │ [Grep] "session" │ test_logout PASSED │ [Edit] README.md │ | |
| │ → 12 matches │ test_token PASSED │ → Updated section │ | |
| │ [Edit] auth.py │ ... │ [Write] CHANGELOG │ | |
| │ → Refactored │ 12 passed │ → Added entry │ | |
| │ │ │ │ | |
| │ Status: Complete │ Status: Complete │ Status: Complete │ | |
| └─────────────────────┴─────────────────────┴─────────────────────┘ | |
| Claude: All three agents have completed: | |
| - Refactor: Simplified auth flow, removed 45 lines of duplicate code | |
| - Tests: All 12 tests passing | |
| - Docs: README and CHANGELOG updated | |
| Cleaning up... | |
| /tmux-agents cleanup | |
| ``` | |
| ### Inter-Agent Communication | |
| For complex workflows where agents need to coordinate: | |
| ```python | |
| class SharedState: | |
| """File-based shared state for agent coordination.""" | |
| STATE_FILE = "/tmp/claude-agents-state.json" | |
| @classmethod | |
| def read(cls) -> dict: | |
| if os.path.exists(cls.STATE_FILE): | |
| return json.loads(Path(cls.STATE_FILE).read_text()) | |
| return {} | |
| @classmethod | |
| def write(cls, key: str, value: any): | |
| state = cls.read() | |
| state[key] = value | |
| Path(cls.STATE_FILE).write_text(json.dumps(state, indent=2)) | |
| @classmethod | |
| def wait_for(cls, key: str, timeout: int = 300) -> any: | |
| """Block until key appears in state.""" | |
| start = time.time() | |
| while time.time() - start < timeout: | |
| state = cls.read() | |
| if key in state: | |
| return state[key] | |
| time.sleep(1) | |
| raise TimeoutError(f"Timed out waiting for {key}") | |
| # Agent 1: Research | |
| SharedState.write("auth_files", ["src/auth.py", "src/session.py"]) | |
| # Agent 2: Waits for research results | |
| auth_files = SharedState.wait_for("auth_files") | |
| for f in auth_files: | |
| # Process files... | |
| ``` | |
| ## User Interaction with Agent Panes | |
| A key advantage of the tmux approach: **the user can interact with any pane at any time**. This isn't a black box - users can observe, intervene, or take over. | |
| ### Switching Between Panes | |
| | Keys | Action | | |
| |------|--------| | |
| | `Ctrl-b ←` | Move to left pane | | |
| | `Ctrl-b →` | Move to right pane | | |
| | `Ctrl-b ↑` | Move to pane above | | |
| | `Ctrl-b ↓` | Move to pane below | | |
| | `Ctrl-b o` | Cycle to next pane | | |
| | `Ctrl-b q` | Show pane numbers (then press 0-9 to jump) | | |
| ### Pane Management | |
| | Keys | Action | | |
| |------|--------| | |
| | `Ctrl-b z` | Zoom current pane (fullscreen toggle) | | |
| | `Ctrl-b x` | Kill current pane (with confirmation) | | |
| | `Ctrl-b !` | Convert pane to new window | | |
| | `Ctrl-b {` | Swap pane left | | |
| | `Ctrl-b }` | Swap pane right | | |
| ### Scrolling and Copy Mode | |
| | Keys | Action | | |
| |------|--------| | |
| | `Ctrl-b [` | Enter scroll/copy mode | | |
| | `q` | Exit scroll mode | | |
| | `↑ ↓` | Scroll line by line (in scroll mode) | | |
| | `PgUp PgDn` | Scroll page by page (in scroll mode) | | |
| | `/` | Search forward (in scroll mode) | | |
| | `?` | Search backward (in scroll mode) | | |
| ### Session Management | |
| | Keys | Action | | |
| |------|--------| | |
| | `Ctrl-b d` | Detach from session (keeps it running) | | |
| | `Ctrl-b $` | Rename session | | |
| | `Ctrl-b s` | List/switch sessions | | |
| ### Reattaching After Detach | |
| ```bash | |
| # List sessions | |
| tmux list-sessions | |
| # Reattach to claude-agents session | |
| tmux attach -t claude-agents | |
| ``` | |
| ### User Intervention Scenarios | |
| #### Scenario 1: Agent is stuck, user takes over | |
| ``` | |
| 1. User sees agent in pane 1 is stuck on a test | |
| 2. User presses Ctrl-b → to switch to pane 1 | |
| 3. User types: Ctrl-C to cancel stuck command | |
| 4. User manually runs: pytest -x tests/specific_test.py | |
| 5. Claude captures result and continues orchestration | |
| ``` | |
| #### Scenario 2: User wants to inspect something | |
| ``` | |
| 1. User sees interesting output in pane 2 | |
| 2. User presses Ctrl-b q, then 2 to jump to pane 2 | |
| 3. User presses Ctrl-b z to zoom (fullscreen) | |
| 4. User presses Ctrl-b [ to scroll up and read history | |
| 5. User presses q to exit scroll mode | |
| 6. User presses Ctrl-b z to unzoom | |
| ``` | |
| #### Scenario 3: User adds a helper command | |
| ``` | |
| 1. User switches to pane 0 | |
| 2. User types: export DEBUG=1 | |
| 3. All subsequent commands in that pane have DEBUG set | |
| 4. Agent continues work with user's environment modification | |
| ``` | |
| ### The Collaboration Model | |
| ``` | |
| ┌─────────────────────────────────────────────────────────────────┐ | |
| │ │ | |
| │ CLAUDE USER │ | |
| │ │ │ │ | |
| │ │ tmux send-keys ────────────────► │ Watches in real-time │ | |
| │ │ │ │ | |
| │ │ │ Can switch panes │ | |
| │ │ │ Can type commands │ | |
| │ │ │ Can Ctrl-C to cancel │ | |
| │ │ │ Can scroll history │ | |
| │ │ │ │ | |
| │ │◄──────────────── tmux capture-pane │ | |
| │ │ Sees user's changes │ │ | |
| │ │ │ │ | |
| │ │ Both working in same terminal │ │ | |
| │ │ True collaboration │ │ | |
| │ │ | |
| └─────────────────────────────────────────────────────────────────┘ | |
| ``` | |
| This is fundamentally different from invisible Task agents. The user isn't just watching - they're a **co-pilot** who can intervene at any moment. | |
| ## Advanced tmux Features for Agentic Workflows | |
| These advanced tmux capabilities unlock sophisticated agent coordination patterns for Phase 3 and Phase 4 implementations. | |
| ### Output Streaming & Capture | |
| | Feature | Command | Agentic Use Case | | |
| |---------|---------|------------------| | |
| | **Pipe-pane** | `tmux pipe-pane -o 'cat >> file.log'` | Stream agent output to file in real-time without polling | | |
| | **Capture full history** | `tmux capture-pane -S -` | Retrieve complete agent session history for analysis | | |
| | **Capture with timestamps** | `tmux capture-pane -p -e` | Capture with escape sequences for colored output preservation | | |
| | **Save buffer to file** | `tmux save-buffer /tmp/out.txt` | Export captured content for external processing | | |
| ### Synchronization & Coordination | |
| | Feature | Command | Agentic Use Case | | |
| |---------|---------|------------------| | |
| | **Wait-for signal** | `tmux wait-for channel-name` | Block agent until another agent signals completion | | |
| | **Send signal** | `tmux wait-for -S channel-name` | Signal other agents that a prerequisite task is done | | |
| | **Lock/unlock** | `tmux wait-for -L/-U lock-name` | Mutex for shared resources between agents | | |
| | **Synchronized panes** | `setw synchronize-panes on` | Send same command to all agents simultaneously (setup, env vars) | | |
| ### Event Hooks | |
| | Feature | Command | Agentic Use Case | | |
| |---------|---------|------------------| | |
| | **Pane exited** | `set-hook pane-exited 'run "script"'` | Detect when agent command finishes and trigger next step | | |
| | **Pane focus** | `set-hook pane-focus-in 'run "script"'` | Detect when user switches to a pane (pause agent?) | | |
| | **Window renamed** | `set-hook window-renamed 'run "script"'` | Track agent status changes via window names | | |
| | **Session created** | `set-hook session-created 'run "script"'` | Initialize environment when orchestrator starts | | |
| | **Client attached** | `set-hook client-attached 'run "script"'` | Notify agents when user starts observing | | |
| | **Alert hooks** | `set-hook alert-activity 'run "script"'` | React when monitored pane has new output | | |
| ### Monitoring & Alerts | |
| | Feature | Command | Agentic Use Case | | |
| |---------|---------|------------------| | |
| | **Monitor activity** | `setw monitor-activity on` | Alert orchestrator when idle agent produces output | | |
| | **Monitor silence** | `setw monitor-silence 30` | Detect stuck agents (no output for N seconds) | | |
| | **Monitor bell** | `setw monitor-bell on` | Agent can ring bell to signal completion/error | | |
| | **Activity action** | `set activity-action other` | Configure how alerts propagate between windows | | |
| ### Pane Control | |
| | Feature | Command | Agentic Use Case | | |
| |---------|---------|------------------| | |
| | **Respawn pane** | `tmux respawn-pane -k "cmd"` | Restart failed agent with new command without losing pane | | |
| | **Kill pane** | `tmux kill-pane -t target` | Terminate runaway agent | | |
| | **Resize pane** | `tmux resize-pane -t target -x 80` | Adjust agent pane size based on workload | | |
| | **Swap panes** | `tmux swap-pane -s src -t dst` | Reorder agents based on priority | | |
| | **Break pane** | `tmux break-pane -t target` | Promote agent pane to its own window | | |
| | **Join pane** | `tmux join-pane -s src -t dst` | Merge agent into another window | | |
| ### Display & UI | |
| | Feature | Command | Agentic Use Case | | |
| |---------|---------|------------------| | |
| | **Display popup** | `tmux display-popup -w 80 "cmd"` | Show floating status overlay without disturbing panes | | |
| | **Display message** | `tmux display-message "text"` | Flash notifications to user about agent status | | |
| | **Select pane title** | `tmux select-pane -T "title"` | Update pane title to show agent status/progress | | |
| | **Status line** | `set status-right "#(script)"` | Dynamic status bar showing agent metrics | | |
| | **Pane border status** | `set pane-border-status top` | Show pane titles in borders for agent identification | | |
| | **Pane border format** | `set pane-border-format "#{pane_title}"` | Customize what shows in pane borders | | |
| ### Layouts & Windows | |
| | Feature | Command | Agentic Use Case | | |
| |---------|---------|------------------| | |
| | **Select layout** | `tmux select-layout tiled` | Auto-arrange agent panes (tiled, even-horizontal, etc.) | | |
| | **Custom layout** | `tmux select-layout "checksum,spec"` | Restore exact pane arrangement for reproducibility | | |
| | **New window** | `tmux new-window -n "name"` | Group related agents in separate windows | | |
| | **Link window** | `tmux link-window -s src -t dst` | Share agent window between sessions | | |
| | **Move window** | `tmux move-window -t target` | Reorganize agent windows | | |
| ### Control Mode & Scripting | |
| | Feature | Command | Agentic Use Case | | |
| |---------|---------|------------------| | |
| | **Control mode** | `tmux -C attach` | Machine-readable event stream for programmatic control | | |
| | **Command sequences** | `tmux source-file script.tmux` | Execute complex multi-command setup from file | | |
| | **If-shell** | `tmux if-shell "test" "cmd1" "cmd2"` | Conditional execution based on environment | | |
| | **Run-shell** | `tmux run-shell "script"` | Execute external scripts with tmux context | | |
| | **Display menu** | `tmux display-menu -T "title" ...` | Interactive menus for user to select agent actions | | |
| ### Session & Client Management | |
| | Feature | Command | Agentic Use Case | | |
| |---------|---------|------------------| | |
| | **Session groups** | `tmux new-session -t existing` | Multiple users observe same agents with different views | | |
| | **Detach other clients** | `tmux detach-client -a` | Ensure single observer for critical operations | | |
| | **Switch client** | `tmux switch-client -t session` | Move orchestrator between agent sessions | | |
| | **List clients** | `tmux list-clients` | Track who is observing agent work | | |
| | **Refresh client** | `tmux refresh-client` | Force UI update after layout changes | | |
| ### Environment & Context | |
| | Feature | Command | Agentic Use Case | | |
| |---------|---------|------------------| | |
| | **Set environment** | `tmux setenv VAR value` | Share configuration across all agent panes | | |
| | **Show environment** | `tmux showenv` | Debug agent environment issues | | |
| | **Update environment** | `tmux setenv -g VAR value` | Global config change affects all agents | | |
| | **Pane environment** | Available in formats | Query individual agent pane environment | | |
| ### Buffer & Clipboard | |
| | Feature | Command | Agentic Use Case | | |
| |---------|---------|------------------| | |
| | **Set buffer** | `tmux set-buffer "text"` | Share data between orchestrator and agents | | |
| | **Get buffer** | `tmux show-buffer` | Retrieve shared data | | |
| | **Paste buffer** | `tmux paste-buffer -t target` | Send data to specific agent pane | | |
| | **Multiple buffers** | `tmux set-buffer -b name "text"` | Named buffers for different data channels | | |
| | **Load buffer** | `tmux load-buffer file.txt` | Import external data for agents | | |
| ### Formats & Variables | |
| | Feature | Example | Agentic Use Case | | |
| |---------|---------|------------------| | |
| | **Pane PID** | `#{pane_pid}` | Track agent process for external monitoring | | |
| | **Pane current command** | `#{pane_current_command}` | See what command agent is running | | |
| | **Pane dead status** | `#{pane_dead}` | Detect crashed agents | | |
| | **Pane last output** | `#{pane_search_string}` | Pattern matching on agent output | | |
| | **Window activity flag** | `#{window_activity_flag}` | Check which agents have new output | | |
| | **Session alerts** | `#{session_alerts}` | Count of agents needing attention | | |
| ### Advanced Patterns | |
| #### Pattern: Agent Completion Detection | |
| ```bash | |
| # Agent signals completion by exiting with specific code | |
| tmux set-hook -t claude-agents pane-exited 'run-shell "echo $TMUX_PANE exited >> /tmp/completions.log"' | |
| ``` | |
| #### Pattern: Agent Output Streaming | |
| ```bash | |
| # Stream all agent output to central log | |
| tmux pipe-pane -t claude-agents:0.0 -o 'cat >> /tmp/agent-0.log' | |
| tmux pipe-pane -t claude-agents:0.1 -o 'cat >> /tmp/agent-1.log' | |
| tmux pipe-pane -t claude-agents:0.2 -o 'cat >> /tmp/agent-2.log' | |
| ``` | |
| #### Pattern: Agent Synchronization | |
| ```bash | |
| # Agent 1: Research (signals when done) | |
| tmux send-keys -t :0.0 'research_cmd && tmux wait-for -S research-done' Enter | |
| # Agent 2: Waits for research, then implements | |
| tmux send-keys -t :0.1 'tmux wait-for research-done && implement_cmd' Enter | |
| ``` | |
| #### Pattern: Stuck Agent Detection | |
| ```bash | |
| # Alert if pane silent for 60 seconds | |
| tmux setw -t claude-agents monitor-silence 60 | |
| tmux set-hook alert-silence 'run-shell "echo STUCK >> /tmp/alerts.log"' | |
| ``` | |
| #### Pattern: User Takeover Detection | |
| ```bash | |
| # Detect when user focuses a pane | |
| tmux set-hook pane-focus-in 'run-shell "echo USER_FOCUS $TMUX_PANE >> /tmp/events.log"' | |
| # Orchestrator can pause sending commands to that pane | |
| ``` | |
| #### Pattern: Dynamic Status Display | |
| ```bash | |
| # Show agent status in pane borders | |
| tmux set pane-border-status top | |
| tmux set pane-border-format '#{pane_index}: #{pane_title} | #{pane_current_command}' | |
| # Update titles as agents progress | |
| tmux select-pane -t :0.0 -T "Agent 1: RUNNING tests" | |
| tmux select-pane -t :0.1 -T "Agent 2: COMPLETE" | |
| ``` | |
| ## Future Enhancements | |
| 1. **Named windows** - Group related agents in windows | |
| 2. **Auto-scaling layout** - Adjust pane sizes based on content | |
| 3. **Progress indicators** - Show completion % in pane titles | |
| 4. **Log persistence** - Save agent output to files | |
| 5. **Inter-agent communication** - Shared state via files/redis | |
| 6. **User input detection** - Claude detects when user types and pauses | |
| 7. **Handoff protocol** - Formal way for user to take over a pane | |
| 8. **Pane annotations** - Floating labels showing agent status |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment