# muxr 0.8: Harness-Aware Session Management

## What muxr is

muxr is an opinionated tmux session manager with optional harness awareness.
It manages the session lifecycle (create, name, switch, save, restore, kill)
and delegates tool-specific operations to harness plugins defined in config.

Without harness config, muxr is a pure tmux manager. With harness config,
operations like rename, save, restore, and upgrade flow through to the
tool running inside the pane.

## What's wrong with 0.7

### Hardcoded claude knowledge
Three modules have claude-specific code:
- `tmux.rs:236`: matches on `"claude"` to add `--name`/`--resume`
- `state.rs`: chains through pgrep + `~/.claude/sessions/{pid}.json`
- `claude_status.rs`: entire module for claude statusline

Adding a second harness means more hardcoded matches.

### Name doesn't flow through
`muxr rename` renames the tmux session but does not rename the
claude session inside it. The harness still shows the old name.
Same gap: if someone creates a session outside muxr, the claude
session has no name at all.

### Health display is naive
The switcher calls `read_health()` for every session. Sessions without
a harness (muxr control plane, shell-only sessions) show bogus
percentages from stale health cache files. The switcher should know
which sessions have a harness and only show health for those.

### No model migration
No way to restart all sessions on a new model. Each session must be
manually switched via `/model`. This blocks version migrations.

### No per-vertical tool binding
`default_tool` is global. Can't say "work verticals use claude,
personal verticals use cursor."

## Design

### Harness config

```toml
default_tool = "claude"

[verticals.work]
dir = "~/projects/work"
color = "#7aa2f7"
tool = "claude"              # optional, overrides default_tool

[verticals.personal]
dir = "~/projects/personal"
color = "#9ece6a"
tool = "opencode"

# Harness definitions. Claude is built-in (compiled default).
# Only define [harnesses.claude] to override the default.

[harnesses.opencode]
bin = "opencode"
session_discovery = "none"
```

**Built-in claude harness** (compiled into muxr, active when
`default_tool = "claude"` or a vertical has `tool = "claude"`):

```rust
HarnessConfig {
    bin: "claude",
    args: ["--name", "{name}"],
    resume_args: ["--resume", "{session_id}"],
    model_args: ["--model", "{model}"],
    rename_command: "/rename {name}",    // sent as keystrokes to pane
    session_discovery: File {
        pattern: "~/.claude/sessions/{pid}.json",
        id_key: "sessionId",
    },
    status_command: Some("muxr claude-status"),
}
```

**Tool resolution:** `--tool` flag > vertical `tool` > `default_tool`

### Command structure

```
# Core tmux management (always available)
muxr <vertical> [context...]      open/attach session
muxr switch                       TUI picker
muxr save                         snapshot all sessions (harness-aware)
muxr restore                      recreate from snapshot (harness-aware)
muxr kill <name>|all              kill sessions
muxr rename <name>                rename session (flows through to harness)
muxr ls                           list sessions
muxr new <vertical> [context...]  create without attaching

# Harness trees (one per configured harness)
muxr claude upgrade [--model <model>]   restart all claude sessions
muxr claude status                      show claude status across sessions
muxr opencode upgrade                   same pattern
```

### Core operations become harness-aware

These existing commands gain harness awareness:

**`muxr rename <name>`**
1. Rename tmux session (existing behavior)
2. If the session has a harness with `rename_command`, send that
   command to the pane (e.g., `/rename tanuki/opus` for claude)

**`muxr save`**
1. Snapshot tmux sessions (existing behavior)
2. For each session with a harness, discover session ID using the
   harness's `session_discovery` config (replaces hardcoded claude logic)

**`muxr restore`**
1. Recreate tmux sessions (existing behavior)
2. Build tool command from harness config (replaces hardcoded claude match)
3. Pass `--resume` and `--name` from harness args config

**Switcher health display**
1. For each session, determine if a harness process is running
   (check if pane has a child process matching any harness `bin`)
2. Only show health bar for sessions with an active harness
3. No more stale health from sessions that used to have claude

### Harness subcommand: upgrade

`muxr claude upgrade [--model <model>]`

For each tmux session running claude:
1. Discover session ID (recursive pgrep walk, verify PID is claude)
2. Send `/exit` to the pane (graceful claude exit)
3. Poll pane for shell prompt (capture-pane, look for prompt chars)
4. Timeout 10s, then SIGKILL if still running
5. Build new launch command: `claude --name '{name}' --resume '{id}' --model '{model}'`
6. Send to pane via tmux send-keys

**Local sessions only.** Remote sessions do not participate.

**Single pane.** Warn if session has multiple panes with harness processes.

All interpolated values are shell-escaped.

### Harness subcommand: status

`muxr claude status`

For each session running claude, display:
- Session name
- Model
- Context usage %
- Cost
- Session age

Reads from health cache files. If no cache, shows "no data".

### Implementation: external_subcommand

Use clap's `#[command(external_subcommand)]` to catch harness names:

```rust
#[derive(Subcommand)]
enum Commands {
    Init, Ls, Save, Restore, Kill { name: String },
    Rename { name: String }, New { args: Vec<String> },
    Switch, TmuxStatus, ClaudeStatus, Completions { shell: String },
    #[command(external_subcommand)]
    External(Vec<std::ffi::OsString>),
}
```

The `External` match arm checks if args[0] is a configured harness
name, then hand-parses the sub-action (upgrade/status) and flags.

Append harness subcommands to `--help` via `Command::after_help()`.

Shell completions extended to include harness names + sub-completions.

### Name collision validation

Config::load() rejects harness names that collide with:
- Vertical names
- Remote names
- Reserved: init, ls, save, restore, new, rename, kill, switch,
  tmux-status, claude-status, completions

### Module changes

| Module | v0.7 | v0.8 |
|--------|------|------|
| config.rs | verticals, remotes, hooks | + HarnessConfig, per-vertical tool, built-in claude default, collision validation |
| main.rs | static Commands enum | + External variant, harness dispatch, after_help |
| tmux.rs | hardcoded claude match | harness config lookup -> built-in -> raw binary |
| state.rs | hardcoded claude discovery | harness-driven discovery, recursive pgrep |
| switcher.rs | read_health for all sessions | health only for sessions with active harness process |
| claude_status.rs | unchanged | unchanged (standalone stdin pipe) |
| **new: harness.rs** | -- | upgrade(), status(), process discovery, graceful restart |
| completions.rs | static subcommands | + harness names + sub-completions from config |

### What doesn't change
- TUI switcher layout and navigation
- Remote/GCE integration
- Pre-create hooks
- claude_status.rs (tmux pipe command)
- The tmux session model (one session = one name = one directory)

### Testing strategy

**Pure function tests (existing pattern):**
- HarnessConfig parsing and validation
- Name collision detection
- Tool resolution order (flag > vertical > default)
- Command building from harness config with template interpolation
- Shell escaping of interpolated values

**Integration tests (new, using --server isolation):**
- Create session with harness, verify tool_command includes --name
- Rename session, verify rename_command sent to pane
- Save/restore round-trip with harness session IDs

### Migration

- Existing configs work unchanged (built-in claude default activates)
- `muxr init` generates config with `[harnesses.claude]` commented out,
  showing the defaults for reference
- v0.7 state.json is compatible (same SavedSession struct, harness
  fields are optional with defaults)
