Introduction
git-paw (Parallel AI Worktrees) orchestrates multiple AI coding CLI sessions across git worktrees from a single terminal using tmux.
Working with AI coding assistants like Claude, Codex, or Gemini is powerful — but what if you could run them in parallel across multiple branches at once? That’s exactly what git-paw does.
The Problem
You have a feature branch, a bugfix branch, and a refactoring branch. You want an AI assistant working on each one simultaneously. Normally you’d need to:
- Open multiple terminals
- Create git worktrees manually
- Navigate to each worktree
- Launch your AI CLI in each one
- Juggle between them
The Solution
With git-paw, you run a single command:
git paw
git-paw will:
- Detect which AI CLIs you have installed (Claude, Codex, Gemini, Aider, etc.)
- Prompt you to pick branches and a CLI (or different CLIs per branch)
- Create git worktrees for each selected branch
- Launch a tmux session with one pane per branch, each running your chosen AI CLI
- Persist the session state so you can stop, resume, or recover after crashes
Key Features
- One command to go from zero to parallel AI sessions
- Smart start — reattaches to active sessions, recovers crashed ones, or launches fresh
- Per-branch CLI selection — use Claude on one branch and Gemini on another
- Session persistence — stop and resume without losing your place
- Custom CLI support — register any AI CLI binary, not just the built-in ones
- Presets — save branch + CLI combos in config for one-command launch
- Non-interactive mode — pass
--cliand--branchesflags for scripting - Dry run — preview what git-paw will do before it does it
How It Works
┌──────────────────────────────────────────────────────┐
│ tmux session │
│ ┌────────────────────┐ ┌────────────────────────┐ │
│ │ feat/auth → claude │ │ feat/api → claude │ │
│ │ │ │ │ │
│ │ (git worktree) │ │ (git worktree) │ │
│ │ │ │ │ │
│ ├────────────────────┤ ├────────────────────────┤ │
│ │ fix/bug → gemini │ │ refactor/db → aider │ │
│ │ │ │ │ │
│ │ (git worktree) │ │ (git worktree) │ │
│ │ │ │ │ │
│ └────────────────────┘ └────────────────────────┘ │
└──────────────────────────────────────────────────────┘
Each pane runs in its own git worktree, so there are no branch conflicts. Your AI assistants work independently and in parallel.
Requirements
- Git (2.20+ recommended for worktree improvements)
- tmux (any recent version)
- At least one AI coding CLI installed (see Supported AI CLIs)
- macOS or Linux (Windows via WSL only)
Next Steps
- Install git-paw
- Quick Start: Same CLI Mode — get running in 2 minutes
- Quick Start: Per-Branch CLI Mode — mix different AI CLIs
Installation
Prerequisites
Before installing git-paw, ensure you have:
- Git 2.20 or later
- tmux — any recent version
Installing tmux
macOS:
brew install tmux
Ubuntu / Debian:
sudo apt install tmux
Fedora:
sudo dnf install tmux
Arch Linux:
sudo pacman -S tmux
Install git-paw
From crates.io (recommended)
cargo install git-paw
Via Homebrew
brew install bearicorn/tap/git-paw
Shell installer
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/bearicorn/git-paw/releases/latest/download/git-paw-installer.sh | sh
From source
git clone https://github.com/bearicorn/git-paw.git
cd git-paw
cargo install --path .
Verify installation
git-paw --version
You should see output like:
git-paw 0.1.0
Since git-paw is named with the git- prefix, git recognizes it as a subcommand. Both of these work:
git-paw --help
git paw --help
Platform Support
| Platform | Support |
|---|---|
| macOS (ARM / Apple Silicon) | Full support |
| macOS (x86_64 / Intel) | Full support |
| Linux (x86_64) | Full support |
| Linux (ARM64 / aarch64) | Full support |
| Windows | WSL only |
Windows (WSL)
git-paw requires tmux, which is not natively available on Windows. Use Windows Subsystem for Linux (WSL):
# Install WSL (PowerShell as admin)
wsl --install
# Then inside WSL:
sudo apt install tmux
cargo install git-paw
All git-paw features work inside WSL. Your AI CLIs must also be installed within the WSL environment.
Install an AI CLI
git-paw needs at least one AI coding CLI installed. See Supported AI CLIs for the full list. Some popular options:
# Claude Code
npm install -g @anthropic-ai/claude-code
# OpenAI Codex
npm install -g @openai/codex
# Aider
pip install aider-chat
Next Steps
Quick Start: Same CLI Mode
This walkthrough shows how to launch git-paw with the same AI CLI on all branches — the most common workflow.
Prerequisites
- git-paw installed
- tmux installed
- At least one AI CLI installed (e.g.,
claude) - A git repository with multiple branches
Step 1: Navigate to your repo
cd ~/projects/my-app
Step 2: Launch git-paw
git paw
Step 3: Select your mode
git-paw presents a mode picker:
? How would you like to assign CLIs to branches?
> Same CLI for all branches
Different CLI per branch
Select Same CLI for all branches and press Enter.
Step 4: Select branches
A multi-select list of all your branches appears with fuzzy search:
? Select branches (space to toggle, enter to confirm):
[ ] main
[x] feat/auth
[x] feat/api
[ ] fix/typo
[x] refactor/db
Use arrow keys to navigate, Space to toggle, and Enter to confirm.
Step 5: Select your CLI
Pick which AI CLI to use on all selected branches:
? Select AI CLI:
> claude
codex
gemini
Step 6: git-paw does the rest
git-paw now:
- Creates a git worktree for each selected branch
- Creates a tmux session named
paw-my-app - Opens one pane per branch
- Launches your chosen CLI in each pane
- Saves the session state for later recovery
Creating worktrees...
✓ my-app-feat-auth (feat/auth)
✓ my-app-feat-api (feat/api)
✓ my-app-refactor-db (refactor/db)
Launching tmux session: paw-my-app
Pane 1: feat/auth → claude
Pane 2: feat/api → claude
Pane 3: refactor/db → claude
Attaching to session...
You’re now inside a tmux session with three panes, each running Claude in its own worktree:
┌─── feat/auth → claude ────────┬─── feat/api → claude ─────────┐
│ │ │
│ Claude is ready to help... │ Claude is ready to help... │
│ │ │
├─── refactor/db → claude ──────┴────────────────────────────────┤
│ │
│ Claude is ready to help... │
│ │
└─────────────────────────────────────────────────────────────────┘
Mouse mode is enabled by default — click a pane to switch to it, or drag borders to resize.
Non-interactive shortcut
Skip all prompts by passing flags:
git paw start --cli claude --branches feat/auth,feat/api,refactor/db
What’s next
- Switch panes: Click with mouse, or use
Ctrl-bthen arrow keys - Detach: Press
Ctrl-b dto detach from tmux (session keeps running) - Reattach: Run
git pawagain — it detects the active session and reattaches - Stop: Run
git paw stopto kill tmux but keep worktrees - Purge: Run
git paw purgeto remove everything
See the User Guide for the full details.
Quick Start: Per-Branch CLI Mode
This walkthrough shows how to assign different AI CLIs to different branches — useful when you want to compare AI assistants or use specialized tools for specific tasks.
Scenario
You have three branches and want to use:
- Claude for the auth feature (complex logic)
- Gemini for the API work (lots of boilerplate)
- Aider for the database refactor (incremental edits)
Step 1: Launch git-paw
cd ~/projects/my-app
git paw
Step 2: Select per-branch mode
? How would you like to assign CLIs to branches?
Same CLI for all branches
> Different CLI per branch
Step 3: Select branches
? Select branches (space to toggle, enter to confirm):
[ ] main
[x] feat/auth
[x] feat/api
[x] refactor/db
Step 4: Assign a CLI to each branch
git-paw prompts you for each branch individually:
? Select CLI for feat/auth:
> claude
codex
gemini
aider
? Select CLI for feat/api:
claude
codex
> gemini
aider
? Select CLI for refactor/db:
claude
codex
gemini
> aider
Step 5: Watch it launch
Creating worktrees...
✓ my-app-feat-auth (feat/auth)
✓ my-app-feat-api (feat/api)
✓ my-app-refactor-db (refactor/db)
Launching tmux session: paw-my-app
Pane 1: feat/auth → claude
Pane 2: feat/api → gemini
Pane 3: refactor/db → aider
Attaching to session...
The tmux session shows each pane with its branch and CLI clearly labeled in the pane border:
┌─── feat/auth → claude ────────┬─── feat/api → gemini ─────────┐
│ │ │
│ Claude is ready to help... │ Gemini is ready... │
│ │ │
├─── refactor/db → aider ───────┴────────────────────────────────┤
│ │
│ Aider v0.x loaded... │
│ │
└─────────────────────────────────────────────────────────────────┘
Comparing approaches
Per-branch mode is great for:
- A/B testing AI assistants — give the same task to Claude and Gemini, compare results
- Specialization — use a code-generation-focused tool for boilerplate and a reasoning-focused tool for complex logic
- Trying new tools — test a new AI CLI on one branch while using your trusted tool on others
Recovery
Session state captures the per-branch CLI assignments. If your terminal closes or tmux crashes:
git paw
git-paw detects the saved session, recreates tmux, and relaunches each branch with its assigned CLI — no re-selection needed.
Next Steps
- User Guide — session management, presets, dry-run, and more
- Configuration — save per-branch presets in config
User Guide
This guide covers the full range of git-paw features beyond the quick starts.
Starting Sessions
Interactive start
Running git paw (or git paw start) with no flags launches the interactive flow:
- Mode selection — same CLI for all branches, or different CLI per branch
- Branch selection — multi-select with fuzzy search
- CLI selection — single pick (uniform) or per-branch assignment
Smart start behavior
git paw start inspects the current repo and decides what to do:
| State | Behavior |
|---|---|
| Active tmux session exists | Reattaches immediately |
| Saved session, tmux dead (crash/reboot) | Auto-recovers: reuses worktrees, recreates tmux, relaunches CLIs |
| No session | Full interactive launch |
You never need to think about whether to “start” or “resume” — just run git paw.
Non-interactive start
Skip prompts with flags:
# Specify both CLI and branches — no prompts at all
git paw start --cli claude --branches feat/auth,feat/api
# Specify just CLI — still prompted for branches
git paw start --cli claude
# Specify just branches — still prompted for CLI
git paw start --branches feat/auth,feat/api
Using presets
Define named presets in your config (see Configuration):
git paw start --preset backend
This uses the branches and CLI defined in the [presets.backend] section of your config.
CLI Modes
Same CLI for all branches
The default mode. Every branch gets the same AI CLI. Best for:
- Working on related features with your preferred tool
- Batch processing branches with a single assistant
Different CLI per branch
Assign a different CLI to each branch. Best for:
- Comparing AI assistants side by side
- Using specialized tools for specific tasks
- Trying a new CLI on one branch while keeping your usual tool on others
Session Management
Checking status
git paw status
Displays the current session state:
Session: paw-my-app
Status: 🟢 active
Created: 2025-01-15T10:30:00Z
Worktrees:
feat/auth → claude (../my-app-feat-auth)
feat/api → claude (../my-app-feat-api)
refactor/db → aider (../my-app-refactor-db)
Status indicators:
- 🟢 active — tmux session is running
- 🟡 stopped — session state saved, tmux not running (recoverable)
- No session — nothing saved for this repo
Stopping a session
git paw stop
This kills the tmux session but preserves:
- Git worktrees (with any uncommitted work)
- Session state file (branch/CLI assignments)
Run git paw later to recover the session with the same setup.
Purging a session
git paw purge
The nuclear option. Removes:
- Tmux session
- All git worktrees created by git-paw
- Session state file
Requires confirmation. Use --force to skip:
git paw purge --force
Dry Run
Preview what git-paw will do without executing:
git paw start --dry-run
Or with flags:
git paw start --cli claude --branches feat/auth,feat/api --dry-run
This runs the detection, selection, and planning steps, then prints the session plan and exits without creating worktrees or tmux sessions.
Tmux Navigation
Once inside a git-paw tmux session:
| Action | Keys |
|---|---|
| Switch pane | Click with mouse, or Ctrl-b + arrow key |
| Resize pane | Drag border with mouse, or Ctrl-b Ctrl-arrow |
| Detach (keep running) | Ctrl-b d |
| Scroll up | Ctrl-b [ then arrow keys, q to exit |
| Zoom pane (fullscreen toggle) | Ctrl-b z |
Mouse mode is enabled by default, so clicking and dragging just works. You can disable it in your config.
One Session Per Repo
git-paw manages one session per repository. If you run git paw in a repo that already has a session, it reattaches rather than creating a second session.
To work with multiple repos simultaneously, open separate terminals and run git paw in each repo directory.
Configuration
git-paw uses TOML configuration files at two levels, with repo-level settings overriding global ones.
Config File Locations
| Level | Path | Purpose |
|---|---|---|
| Global | ~/.config/git-paw/config.toml | Default CLI, custom CLIs, global presets |
| Per-repo | .git-paw/config.toml (in repo root) | Repo-specific overrides |
Both files are optional. git-paw works with sensible defaults when no config exists.
Full Config Example
# Default CLI used when --cli flag is not provided
default_cli = "claude"
# Enable mouse mode in tmux sessions (default: true)
mouse = true
# Custom CLI definitions
[clis.my-agent]
command = "/usr/local/bin/my-agent"
display_name = "My Agent"
[clis.local-llm]
command = "ollama-code"
display_name = "Local LLM"
# Named presets for quick launch
[presets.backend]
branches = ["feature/api", "fix/db"]
cli = "claude"
[presets.frontend]
branches = ["feature/ui", "feature/styles"]
cli = "codex"
Settings Reference
default_cli
The AI CLI to use when --cli is not passed and you want to skip the CLI picker.
default_cli = "claude"
mouse
Enable or disable tmux mouse mode for git-paw sessions. When enabled, you can click panes to switch, drag borders to resize, and scroll with the mouse wheel. This is set per-session and does not affect your other tmux sessions.
mouse = true # default
Custom CLIs
Register custom AI CLIs that aren’t in git-paw’s built-in detection list.
Via config file
[clis.my-agent]
command = "/usr/local/bin/my-agent" # absolute path
display_name = "My Agent" # optional, shown in prompts
[clis.local-llm]
command = "ollama-code" # binary name (resolved via PATH)
display_name = "Local LLM"
Via command line
# Add with absolute path
git paw add-cli my-agent /usr/local/bin/my-agent
# Add with binary name on PATH
git paw add-cli my-agent my-agent
# Add with display name
git paw add-cli my-agent my-agent --display-name "My Agent"
# Remove
git paw remove-cli my-agent
The add-cli and remove-cli commands modify the global config at ~/.config/git-paw/config.toml.
Listing CLIs
git paw list-clis
Shows both auto-detected and custom CLIs with their source:
Name Path Source
claude /usr/local/bin/claude detected
codex /usr/local/bin/codex detected
my-agent /usr/local/bin/my-agent custom
Presets
Presets save branch + CLI combinations for one-command launch.
Defining presets
[presets.backend]
branches = ["feature/api", "fix/db-migration"]
cli = "claude"
[presets.full-stack]
branches = ["feature/api", "feature/ui", "feature/styles"]
cli = "gemini"
Using presets
git paw start --preset backend
This skips all interactive prompts and launches with the preset’s branches and CLI.
Merging Rules
When both global and repo configs exist, they merge with these rules:
| Field | Merge behavior |
|---|---|
default_cli | Repo wins |
mouse | Repo wins |
clis | Maps merge (repo overrides per-key) |
presets | Maps merge (repo overrides per-key) |
Example: If global config defines [clis.my-agent] and repo config defines [clis.my-agent] with a different command, the repo version wins. But a [clis.other-tool] in global config still appears — maps are merged, not replaced.
Graceful Absence
If no config files exist, git-paw uses defaults:
- No default CLI (prompts for selection)
- Mouse mode enabled
- No custom CLIs
- No presets
Supported AI CLIs
git-paw auto-detects these AI coding CLIs on your PATH:
| CLI | Binary | Description | Install |
|---|---|---|---|
| Claude Code | claude | Anthropic’s AI coding assistant | npm i -g @anthropic-ai/claude-code |
| Codex | codex | OpenAI’s coding agent | npm i -g @openai/codex |
| Gemini CLI | gemini | Google’s Gemini in the terminal | npm i -g @anthropic-ai/gemini-cli |
| Aider | aider | AI pair programming in the terminal | pip install aider-chat |
| Vibe | vibe | Mistral AI’s coding CLI | See project docs |
| Qwen | qwen | Alibaba’s Qwen coding CLI | See project docs |
| Amp | amp | Sourcegraph’s AI coding agent | See project docs |
How Detection Works
git-paw scans your PATH for each known binary name. If found, it records the full path and makes the CLI available for selection.
Detection runs every time you start a session, so newly installed CLIs are picked up automatically.
Adding Custom CLIs
Any AI CLI not in the list above can be registered as a custom CLI:
# Register by path
git paw add-cli my-agent /usr/local/bin/my-agent
# Register by binary name (resolved via PATH)
git paw add-cli my-agent my-agent --display-name "My Agent"
Custom CLIs appear alongside detected ones in the selection prompt. See Configuration for more details.
Deduplication
If a custom CLI has the same binary name as a detected one, the custom definition takes precedence. This lets you override the path or display name of a detected CLI.
Missing CLIs
If a custom CLI’s command cannot be found (the binary doesn’t exist at the specified path and isn’t on PATH), it is excluded from the selection list with a warning. This prevents launching sessions that would immediately fail.
CLI Reference
git-paw is invoked as git paw (or git-paw). Below is the reference for all subcommands and flags.
git paw
Running with no subcommand is equivalent to git paw start.
Parallel AI Worktrees — orchestrate multiple AI coding CLI sessions across git worktrees
Usage: git-paw [COMMAND]
Commands:
start Launch a new session or reattach to an existing one
stop Stop the session (kills tmux, keeps worktrees and state)
purge Remove everything (tmux session, worktrees, and state)
status Show session state for the current repo
list-clis List detected and custom AI CLIs
add-cli Register a custom AI CLI
remove-cli Unregister a custom AI CLI
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print version
git paw start
Smart start: reattaches if a session is active, recovers if stopped/crashed, or launches a new interactive session.
Usage: git-paw start [OPTIONS]
Options:
--cli <CLI> AI CLI to use (skips CLI picker)
--branches <BRANCHES> Comma-separated branches (skips branch picker)
--dry-run Preview the session plan without executing
--preset <PRESET> Use a named preset from config
-h, --help Print help
Examples:
git paw start
git paw start --cli claude
git paw start --cli claude --branches feat/auth,feat/api
git paw start --dry-run
git paw start --preset backend
git paw stop
Kills the tmux session but preserves worktrees and session state on disk. Run git paw start later to recover the session.
Usage: git-paw stop
Options:
-h, --help Print help
Example:
git paw stop
git paw purge
Nuclear option: kills the tmux session, removes all worktrees, and deletes session state. Requires confirmation unless --force is used.
Usage: git-paw purge [OPTIONS]
Options:
--force Skip confirmation prompt
-h, --help Print help
Examples:
git paw purge
git paw purge --force
git paw status
Displays the current session status, branches, CLIs, and worktree paths for the repository in the current directory.
Usage: git-paw status
Options:
-h, --help Print help
Example:
git paw status
git paw list-clis
Shows all AI CLIs found on PATH (auto-detected) and any custom CLIs registered in your config.
Usage: git-paw list-clis
Options:
-h, --help Print help
Example:
git paw list-clis
git paw add-cli
Adds a custom CLI to your global config (~/.config/git-paw/config.toml). The command can be an absolute path or a binary name on PATH.
Usage: git-paw add-cli [OPTIONS] <NAME> <COMMAND>
Arguments:
<NAME> Name to register the CLI as
<COMMAND> Command or path to the CLI binary
Options:
--display-name <DISPLAY_NAME> Display name shown in prompts
-h, --help Print help
Examples:
git paw add-cli my-agent /usr/local/bin/my-agent
git paw add-cli my-agent my-agent --display-name "My Agent"
git paw remove-cli
Removes a custom CLI from your global config. Only custom CLIs can be removed — auto-detected CLIs cannot.
Usage: git-paw remove-cli <NAME>
Arguments:
<NAME> Name of the custom CLI to remove
Options:
-h, --help Print help
Example:
git paw remove-cli my-agent
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Error (git, tmux, config, or other failure) |
| 2 | User cancelled (Ctrl+C or empty selection) |
Architecture
This chapter covers git-paw’s internal architecture: module structure, data flow, and key design decisions.
Module Diagram
┌─────────────────────────────────────────────────┐
│ main.rs │
│ (entry point, dispatch) │
├────────┬──────────┬───────────┬─────────────────┤
│ │ │ │ │
│ cli.rs interactive.rs config.rs error.rs │
│ (clap) (dialoguer UI) (TOML) (PawError) │
│ │ │ │ │
├────────┴──────────┴───────────┴─────────────────┤
│ │
│ detect.rs git.rs tmux.rs session.rs │
│ (PATH scan) (worktrees) (builder) (JSON) │
│ │
└───────────────────────────────────────────────────┘
Module Responsibilities
| Module | File | Purpose |
|---|---|---|
| CLI | src/cli.rs | Argument parsing with clap v4 derive macros. Defines all subcommands, flags, and help text. |
| Detection | src/detect.rs | Scans PATH for 8 known AI CLI binaries. Resolves custom CLIs from config. Merges and deduplicates. |
| Git | src/git.rs | Validates git repos, lists branches (local + remote, deduplicated), creates/removes worktrees, derives safe directory names. |
| Tmux | src/tmux.rs | Builder pattern for tmux operations. Creates sessions, splits panes, sends commands, applies tiled layout, sets pane titles. |
| Session | src/session.rs | Persists session state to JSON files under ~/.local/share/git-paw/sessions/. Atomic writes, crash recovery. |
| Config | src/config.rs | Parses TOML from global (~/.config/git-paw/config.toml) and per-repo (.git-paw/config.toml). Merges with repo-wins semantics. |
| Interactive | src/interactive.rs | Terminal prompts via dialoguer. Mode picker, branch multi-select, CLI picker. Skips prompts when flags are provided. |
| Error | src/error.rs | PawError enum with thiserror. Actionable error messages and distinct exit codes. |
Start Flow
The start command is the primary flow. Here’s what happens step by step:
git paw start
│
▼
┌─ Check for existing session ──────────────────────┐
│ │
│ Session active + tmux alive? ──yes──► Reattach │
│ │ no │
│ Session saved + tmux dead? ──yes──► Recover │
│ │ no │
│ No session ──────► Fresh start │
└─────────────────────────────────────────────────────┘
│
▼ (fresh start)
┌─ Validate git repo ─────────────────────────────────┐
│ git.validate_repo() → repo root path │
└──────────────────────────────────────────────────────┘
│
▼
┌─ Load config ────────────────────────────────────────┐
│ config.load_config() → merged PawConfig │
└──────────────────────────────────────────────────────┘
│
▼
┌─ Detect CLIs ────────────────────────────────────────┐
│ detect.detect_clis() → Vec<CliInfo> │
│ (auto-detected + custom, deduplicated) │
└──────────────────────────────────────────────────────┘
│
▼
┌─ Interactive selection ──────────────────────────────┐
│ interactive.run_selection() │
│ → Vec<(branch, cli)> mappings │
│ (skipped if --cli + --branches provided) │
└──────────────────────────────────────────────────────┘
│
▼
┌─ Create worktrees ───────────────────────────────────┐
│ git.create_worktree() for each branch │
│ → ../project-branch-name/ directories │
└──────────────────────────────────────────────────────┘
│
▼
┌─ Build tmux session ────────────────────────────────┐
│ TmuxSessionBuilder │
│ .session_name("paw-project") │
│ .pane(branch, worktree, cli) × N │
│ .mouse(true) │
│ .build() → TmuxSession with command sequence │
└──────────────────────────────────────────────────────┘
│
▼
┌─ Save session state ────────────────────────────────┐
│ session.save_session() → atomic JSON write │
└──────────────────────────────────────────────────────┘
│
▼
┌─ Attach ─────────────────────────────────────────────┐
│ tmux.attach() → user enters tmux session │
└──────────────────────────────────────────────────────┘
Worktree Lifecycle
Git worktrees are the foundation of git-paw’s parallel workflow.
Creation
For a project named my-app and branch feature/auth-flow:
my-app/ ← main repo (current directory)
my-app-feature-auth-flow/ ← worktree (created by git-paw)
my-app-feat-api/ ← worktree (created by git-paw)
Worktrees are created as siblings of the main repo directory. The naming convention is <project>-<sanitized-branch> where slashes become hyphens.
Lifecycle states
create_worktree() stop start (recover)
│ │ │
▼ ▼ ▼
[exists on disk] → [still on disk] → [reused as-is]
│
purge │
▼
[removed from disk]
Key points:
- Stop preserves worktrees — uncommitted work survives
- Recover reuses existing worktrees — no data loss
- Purge removes worktrees —
git worktree removefollowed by prune
Session State
Session state is persisted as JSON under ~/.local/share/git-paw/sessions/:
{
"session_name": "paw-my-app",
"repo_path": "/Users/you/projects/my-app",
"project_name": "my-app",
"created_at": "2025-01-15T10:30:00Z",
"status": "active",
"worktrees": [
{
"branch": "feat/auth",
"worktree_path": "/Users/you/projects/my-app-feat-auth",
"cli": "claude"
},
{
"branch": "feat/api",
"worktree_path": "/Users/you/projects/my-app-feat-api",
"cli": "gemini"
}
]
}
Atomic writes
Session state is written atomically: write to a temporary file, then rename. This prevents corruption if the process is killed mid-write.
Effective status
The on-disk status may not reflect reality (e.g., tmux was killed externally). git-paw checks the actual tmux state:
| File status | tmux alive? | Effective status |
|---|---|---|
active | Yes | Active (reattach) |
active | No | Stopped (recover) |
stopped | N/A | Stopped (recover) |
| No file | N/A | No session |
Tmux Builder Pattern
The tmux module uses a builder pattern that accumulates operations as data structures rather than immediately executing shell commands. This enables:
- Testability — generate commands without executing them
- Dry run — print the plan without side effects
- Atomicity — validate the full plan before running anything
#![allow(unused)]
fn main() {
TmuxSessionBuilder::new()
.session_name("paw-my-app")
.pane(PaneSpec { branch, worktree_path, cli_command })
.pane(PaneSpec { ... })
.mouse(true)
.build()
// → TmuxSession { name, commands: Vec<TmuxCommand> }
}
The built TmuxSession can be inspected, printed (dry run), or executed.
Error Strategy
All errors flow through PawError (defined with thiserror). Each variant carries an actionable message telling the user what went wrong and how to fix it. No panics in non-test code — all Result propagation.
Exit codes:
- 0 — success
- 1 — operational error
- 2 — user cancelled
Specifications
git-paw uses OpenSpec for formal, testable specifications. Each capability has a dedicated spec file using RFC 2119 keywords (SHALL, MUST, SHOULD) and GIVEN/WHEN/THEN scenarios.
Specification Index
| Capability | Description |
|---|---|
| CLI Parsing | Command-line argument parsing and subcommands |
| CLI Detection | Auto-detect AI CLIs on PATH, load custom CLIs |
| Git Operations | Validate repos, list branches, manage worktrees |
| Tmux Orchestration | Create sessions, manage panes, apply layout |
| Session State | Persist and recover session state |
| Configuration | Parse and merge TOML config files |
| Interactive Selection | User prompts for mode, branch, and CLI selection |
| Error Handling | Unified error types with exit codes |
CLI Parsing
Purpose
Define the command-line interface for git-paw using clap v4. Declares all subcommands (start, stop, purge, status, list-clis, add-cli, remove-cli), their flags, and argument validation. When no subcommand is given, defaults to start.
Requirements
Requirement: Default to start when no subcommand is given
The system SHALL treat no arguments as equivalent to start with no flags.
Scenario: No arguments yields None command
- GIVEN no arguments are passed
- WHEN the CLI is parsed
- THEN
commandSHALL beNone(handled asStartin main)
Test: cli::tests::no_args_defaults_to_none_command
Requirement: Start subcommand with optional flags
The start subcommand SHALL accept --cli, --branches (comma-separated), --dry-run, and --preset flags, all optional.
Scenario: Start with no flags
- GIVEN
startis passed with no flags - WHEN the CLI is parsed
- THEN all optional fields SHALL be
None/false
Test: cli::tests::start_with_no_flags
Scenario: Start with –cli flag
- GIVEN
start --cli claude - WHEN the CLI is parsed
- THEN
cliSHALL beSome("claude")
Test: cli::tests::start_with_cli_flag
Scenario: Start with comma-separated –branches flag
- GIVEN
start --branches feat/a,feat/b,fix/c - WHEN the CLI is parsed
- THEN
branchesSHALL be["feat/a", "feat/b", "fix/c"]
Test: cli::tests::start_with_branches_flag_comma_separated
Scenario: Start with –dry-run flag
- GIVEN
start --dry-run - WHEN the CLI is parsed
- THEN
dry_runSHALL betrue
Test: cli::tests::start_with_dry_run
Scenario: Start with –preset flag
- GIVEN
start --preset backend - WHEN the CLI is parsed
- THEN
presetSHALL beSome("backend")
Test: cli::tests::start_with_preset
Scenario: Start with all flags combined
- GIVEN
start --cli gemini --branches a,b --dry-run --preset dev - WHEN the CLI is parsed
- THEN all fields SHALL be populated correctly
Test: cli::tests::start_with_all_flags
Requirement: Stop subcommand
The stop subcommand SHALL parse with no additional arguments.
Scenario: Stop parses
- GIVEN
stopis passed - WHEN the CLI is parsed
- THEN the command SHALL be
Command::Stop
Test: cli::tests::stop_parses
Requirement: Purge subcommand with optional –force flag
The purge subcommand SHALL accept an optional --force flag (defaults to false).
Scenario: Purge without –force
- GIVEN
purgeis passed without flags - WHEN the CLI is parsed
- THEN
forceSHALL befalse
Test: cli::tests::purge_without_force
Scenario: Purge with –force
- GIVEN
purge --forceis passed - WHEN the CLI is parsed
- THEN
forceSHALL betrue
Test: cli::tests::purge_with_force
Requirement: Status subcommand
The status subcommand SHALL parse with no additional arguments.
Scenario: Status parses
- GIVEN
statusis passed - WHEN the CLI is parsed
- THEN the command SHALL be
Command::Status
Test: cli::tests::status_parses
Requirement: List-CLIs subcommand
The list-clis subcommand SHALL parse with no additional arguments.
Scenario: List-CLIs parses
- GIVEN
list-clisis passed - WHEN the CLI is parsed
- THEN the command SHALL be
Command::ListClis
Test: cli::tests::list_clis_parses
Requirement: Add-CLI subcommand with required and optional arguments
The add-cli subcommand SHALL require name and command positional arguments and accept an optional --display-name flag.
Scenario: Add-CLI with required arguments only
- GIVEN
add-cli my-agent /usr/local/bin/my-agent - WHEN the CLI is parsed
- THEN
nameSHALL be"my-agent",commandSHALL be the path, anddisplay_nameSHALL beNone
Test: cli::tests::add_cli_with_required_args
Scenario: Add-CLI with –display-name
- GIVEN
add-cli my-agent my-agent --display-name "My Agent" - WHEN the CLI is parsed
- THEN
display_nameSHALL beSome("My Agent")
Test: cli::tests::add_cli_with_display_name
Scenario: Add-CLI missing required arguments is rejected
- GIVEN
add-cliwith no positional arguments - WHEN the CLI is parsed
- THEN parsing SHALL fail
Test: cli::tests::add_cli_missing_required_args_is_rejected
Requirement: Remove-CLI subcommand with required argument
The remove-cli subcommand SHALL require a name positional argument.
Scenario: Remove-CLI parses
- GIVEN
remove-cli my-agent - WHEN the CLI is parsed
- THEN
nameSHALL be"my-agent"
Test: cli::tests::remove_cli_parses
Requirement: Standard flags –version and –help
The CLI SHALL accept --version and --help flags.
Scenario: –version flag is accepted
- GIVEN
--versionis passed - WHEN the CLI is parsed
- THEN clap SHALL emit a
DisplayVersionresponse
Test: cli::tests::version_flag_is_accepted
Scenario: –help flag is accepted
- GIVEN
--helpis passed - WHEN the CLI is parsed
- THEN clap SHALL emit a
DisplayHelpresponse
Test: cli::tests::help_flag_is_accepted
Requirement: Unknown subcommands are rejected
The CLI SHALL reject unrecognized subcommands with a parse error.
Scenario: Unknown subcommand fails
- GIVEN an unrecognized subcommand is passed
- WHEN the CLI is parsed
- THEN parsing SHALL fail
Test: cli::tests::unknown_subcommand_is_rejected
Requirement: Help output contains all subcommands and quick start
The --help output SHALL list all subcommands and include a Quick Start section.
Scenario: Help lists all subcommands
- GIVEN
--helpis passed - WHEN the binary runs
- THEN stdout SHALL contain start, stop, purge, status, list-clis, add-cli, and remove-cli
Test: cli_tests::help_shows_all_subcommands
Scenario: Help contains Quick Start
- GIVEN
--helpis passed - WHEN the binary runs
- THEN stdout SHALL contain “Quick Start”
Test: cli_tests::help_contains_quick_start
Scenario: Start help shows all flags
- GIVEN
start --helpis passed - WHEN the binary runs
- THEN stdout SHALL contain –cli, –branches, –dry-run, and –preset
Test: cli_tests::start_help_shows_flags
Scenario: Purge help shows –force flag
- GIVEN
purge --helpis passed - WHEN the binary runs
- THEN stdout SHALL contain –force
Test: cli_tests::purge_help_shows_force_flag
Scenario: Add-CLI help shows arguments
- GIVEN
add-cli --helpis passed - WHEN the binary runs
- THEN stdout SHALL contain –display-name, name, and command arguments
Test: cli_tests::add_cli_help_shows_arguments
Requirement: Version output includes binary name
The --version output SHALL include the binary name.
Scenario: Version output
- GIVEN
--versionis passed - WHEN the binary runs
- THEN stdout SHALL contain “git-paw”
Test: cli_tests::version_output
Requirement: No arguments behaves like start
When no subcommand is provided, the binary SHALL behave identically to start.
Scenario: No args produces same error as start
- GIVEN the binary is run with no arguments outside a git repo
- WHEN both
git-pawandgit-paw startare run - THEN they SHALL produce identical stderr output
Test: cli_tests::no_args_behaves_like_start
Requirement: Subcommands run without error when applicable
Subcommands that don’t require a session SHALL succeed in a valid git repo.
Scenario: Stop runs without error
- GIVEN the binary is run in a git repo
- WHEN
stopis passed - THEN it SHALL succeed
Test: cli_tests::stop_runs_without_error
Scenario: Status runs without error
- GIVEN the binary is run in a git repo
- WHEN
statusis passed - THEN it SHALL succeed
Test: cli_tests::status_runs_without_error
Scenario: List-CLIs runs without error
- GIVEN the binary is run in a git repo
- WHEN
list-clisis passed - THEN it SHALL succeed
Test: cli_tests::list_clis_runs_without_error
Requirement: Binary rejects missing required arguments
Subcommands with required arguments SHALL fail when they are missing.
Scenario: Add-CLI requires arguments
- GIVEN
add-cliis passed with no arguments - WHEN the binary runs
- THEN it SHALL fail with stderr mentioning “required”
Test: cli_tests::add_cli_requires_arguments
Scenario: Remove-CLI requires argument
- GIVEN
remove-cliis passed with no arguments - WHEN the binary runs
- THEN it SHALL fail with stderr mentioning “required”
Test: cli_tests::remove_cli_requires_argument
Requirement: Not-a-repo error from binary
Commands requiring a git repo SHALL fail with an actionable error when run outside one.
Scenario: Start from non-git directory
- GIVEN the binary is run outside a git repository
- WHEN
startis passed - THEN it SHALL fail with stderr containing “Not a git repository”
Test: cli_tests::start_from_non_git_dir
Scenario: Unknown subcommand from binary
- GIVEN the binary is passed an unrecognized subcommand
- WHEN it runs
- THEN it SHALL fail with stderr containing “error”
Test: cli_tests::unknown_subcommand_fails
CLI Detection
Purpose
Detect available AI coding CLI binaries by scanning PATH for known names and merging with user-defined custom CLIs from configuration. Provides a unified, deduplicated, sorted list for interactive selection or direct use.
Requirements
Requirement: Auto-detect known AI CLIs on PATH
The system SHALL scan PATH for the known CLI binaries: claude, codex, gemini, aider, vibe, qwen, and amp.
Scenario: All known CLIs are present on PATH
- GIVEN all 8 known CLI binaries exist on PATH
- WHEN
detect_known_clis()is called - THEN it SHALL return a
CliInfofor each binary withsource = Detected, a non-emptydisplay_name, and a validpath
Test: detect::tests::all_known_clis_detected_when_present
Scenario: No known CLIs are present on PATH
- GIVEN PATH contains no known CLI binaries
- WHEN
detect_known_clis()is called - THEN it SHALL return an empty list
Test: detect::tests::returns_empty_when_no_known_clis_on_path
Scenario: Partial set of CLIs on PATH
- GIVEN only a subset of known CLIs exist on PATH
- WHEN
detect_known_clis()is called - THEN it SHALL return only the CLIs that are found
Test: detect::tests::detects_subset_of_known_clis
Requirement: Resolve and merge custom CLIs from configuration
The system SHALL resolve custom CLI definitions by looking up commands as absolute paths or via PATH, and merge them with auto-detected CLIs.
Scenario: Custom CLIs merged with detected CLIs
- GIVEN auto-detected CLIs exist and custom CLI definitions are provided
- WHEN
detect_clis()is called - THEN the result SHALL contain both detected and custom CLIs
Test: detect::tests::custom_clis_merged_with_detected
Scenario: Custom CLI binary not found
- GIVEN a custom CLI definition references a non-existent binary
- WHEN
detect_clis()is called - THEN the missing CLI SHALL be excluded and a warning printed to stderr
Test: detect::tests::custom_cli_excluded_when_binary_missing
Scenario: Custom CLI resolved by absolute path
- GIVEN a custom CLI definition uses an absolute path to an existing binary
- WHEN
resolve_custom_clis()is called - THEN the resolved path SHALL match the absolute path provided
Test: detect::tests::custom_cli_resolved_by_absolute_path
Requirement: Custom CLIs override detected CLIs with the same name
When a custom CLI has the same binary_name as a detected CLI, the custom definition SHALL take precedence.
Scenario: Custom CLI overrides auto-detected CLI
- GIVEN a custom CLI shares a
binary_namewith an auto-detected CLI - WHEN
detect_clis()is called - THEN the result SHALL contain only the custom version with
source = Custom
Test: detect::tests::custom_cli_overrides_detected_with_same_binary_name
Requirement: Each CLI result includes all required fields
Every CliInfo SHALL have a non-empty display_name, binary_name, a valid path, and a source indicator.
Scenario: Detected CLI has all fields populated
- GIVEN a known CLI binary exists on PATH
- WHEN it is detected
- THEN all fields (
display_name,binary_name,path,source) SHALL be populated
Test: detect::tests::detected_cli_has_all_fields
Scenario: Custom CLI has all fields populated
- GIVEN a custom CLI definition is resolved
- WHEN it is included in results
- THEN all fields SHALL be populated
Test: detect::tests::custom_cli_has_all_fields
Requirement: Display name derivation
When no explicit display name is provided, the system SHALL derive one by capitalizing the first letter of the binary name.
Scenario: Custom CLI defaults to capitalized name
- GIVEN a custom CLI definition has no
display_name - WHEN it is resolved
- THEN the
display_nameSHALL be the binary name with the first letter capitalized
Test: detect::tests::custom_cli_display_name_defaults_to_capitalised_name
Requirement: Results sorted by display name
The combined CLI list SHALL be sorted alphabetically by display_name (case-insensitive).
Scenario: Results are sorted
- GIVEN multiple CLIs are detected and/or custom
- WHEN
detect_clis()is called - THEN the results SHALL be sorted by display name
Test: detect::tests::results_sorted_by_display_name
Requirement: CliSource display format
The CliSource enum SHALL display as "detected" or "custom".
Scenario: CliSource display strings
- GIVEN
CliSource::DetectedandCliSource::Custom - WHEN formatted with
Display - THEN they SHALL render as
"detected"and"custom"respectively
Test: detect::tests::cli_source_display_format
Git Operations
Purpose
Validate git repositories, list branches, create and remove worktrees, and derive worktree directory names. Provides the git plumbing that underpins parallel branch sessions.
Requirements
Requirement: Validate that a path is inside a git repository
The system SHALL confirm a path is inside a git repository and return the repository root.
Scenario: Path is inside a git repository
- GIVEN a path inside an initialized git repository
- WHEN
validate_repo()is called - THEN it SHALL return
Okwith the absolute path to the repository root
Test: git::tests::validate_repo_returns_root_inside_repo
Scenario: Path is not inside a git repository
- GIVEN a path that is not inside any git repository
- WHEN
validate_repo()is called - THEN it SHALL return
Err(PawError::NotAGitRepo)
Test: git::tests::validate_repo_returns_not_a_git_repo_outside
Requirement: List branches sorted and deduplicated
The system SHALL list all local and remote branches, deduplicated, sorted, with remote prefixes stripped and HEAD pointers excluded.
Scenario: Branches are returned sorted
- GIVEN a repository with multiple branches
- WHEN
list_branches()is called - THEN it SHALL return branches sorted alphabetically
Test: git::tests::list_branches_returns_sorted_branches
Scenario: Local and remote branches are deduplicated with prefix stripping
- GIVEN a repository cloned from a remote, with branches existing both locally and as remote-tracking refs
- WHEN
list_branches()is called - THEN each branch SHALL appear exactly once, with
origin/prefixes stripped
Test: git_integration::list_branches_strips_remote_prefix_and_deduplicates
Requirement: Derive project name from repository path
The system SHALL extract the project name from the final component of the repository root path, falling back to "project" for root paths.
Scenario: Normal repository path
- GIVEN a repository at
/Users/jie/code/git-paw - WHEN
project_name()is called - THEN it SHALL return
"git-paw"
Test: git::tests::project_name_from_path
Scenario: Root path fallback
- GIVEN a repository at
/ - WHEN
project_name()is called - THEN it SHALL return
"project"
Test: git::tests::project_name_fallback_for_root
Requirement: Build worktree directory names
The system SHALL generate worktree directory names as <project>-<sanitized-branch>, replacing / with - and stripping unsafe characters.
Scenario: Branch with single slash
- GIVEN project
"git-paw"and branch"feature/auth-flow" - WHEN
worktree_dir_name()is called - THEN it SHALL return
"git-paw-feature-auth-flow"
Test: git::tests::worktree_dir_name_replaces_slash_with_dash
Scenario: Branch with multiple slashes
- GIVEN project
"git-paw"and branch"feat/auth/v2" - WHEN
worktree_dir_name()is called - THEN it SHALL return
"git-paw-feat-auth-v2"
Test: git::tests::worktree_dir_name_handles_multiple_slashes
Scenario: Branch with special characters
- GIVEN project
"my-proj"and branch"fix/issue#42" - WHEN
worktree_dir_name()is called - THEN unsafe characters SHALL be stripped, returning
"my-proj-fix-issue42"
Test: git::tests::worktree_dir_name_strips_special_chars
Scenario: Simple branch name
- GIVEN project
"git-paw"and branch"main" - WHEN
worktree_dir_name()is called - THEN it SHALL return
"git-paw-main"
Test: git::tests::worktree_dir_name_simple_branch
Requirement: Create worktrees as siblings of the repository
The system SHALL create git worktrees in the parent directory of the repository root using the derived directory name convention.
Scenario: Worktree created at correct path
- GIVEN a repository with a branch
feature/test - WHEN
create_worktree()is called - THEN a worktree SHALL be created at
../<project>-feature-testcontaining the repository files
Test: git::tests::create_worktree_at_correct_path
Scenario: Creating worktree for currently checked-out branch fails
- GIVEN the current branch is checked out in the main repo
- WHEN
create_worktree()is called for that branch - THEN it SHALL return
Err(PawError::WorktreeError)
Test: git::tests::create_worktree_errors_on_checked_out_branch
Requirement: Remove worktrees and prune stale entries
The system SHALL force-remove a worktree and prune stale git worktree metadata.
Scenario: Worktree fully cleaned up after removal
- GIVEN an existing worktree
- WHEN
remove_worktree()is called - THEN the directory SHALL be deleted and git SHALL no longer track it
Test: git::tests::remove_worktree_cleans_up_fully
Requirement: Repository validation SHALL work against real git repos
Scenario: Succeeds inside a real git repo
- GIVEN a temporary git repository with an initial commit
- WHEN
validate_repo()is called - THEN it SHALL return the canonicalized repo root
Test: git_integration::validate_repo_succeeds_inside_git_repo
Scenario: Fails outside a git repo
- GIVEN a temporary directory that is not a git repo
- WHEN
validate_repo()is called - THEN it SHALL return an error
Test: git_integration::validate_repo_fails_outside_git_repo
Requirement: Branch listing SHALL work against real git repos
Scenario: Lists created branches
- GIVEN a repo with branches
feature/authandfix/db - WHEN
list_branches()is called - THEN both branches SHALL appear in the result
Test: git_integration::list_branches_includes_created_branches
Scenario: Branches are sorted
- GIVEN branches created in non-alphabetical order
- WHEN
list_branches()is called - THEN results SHALL be alphabetically sorted
Test: git_integration::list_branches_returns_sorted
Scenario: Deduplicates local and remote
- GIVEN a repository with a default branch
- WHEN
list_branches()is called - THEN each branch SHALL appear exactly once
Test: git_integration::list_branches_deduplicates_local_and_remote
Requirement: Worktree lifecycle SHALL work against real git repos
Scenario: Create and remove worktree
- GIVEN a branch in a temporary repo
- WHEN
create_worktree()thenremove_worktree()are called - THEN the worktree SHALL exist after creation and be gone after removal
Test: git_integration::create_and_remove_worktree
Scenario: Worktree placed as sibling of repo
- GIVEN a repo at
<sandbox>/test-repo/ - WHEN
create_worktree()is called - THEN the worktree SHALL be in the same parent directory
Test: git_integration::worktree_placed_as_sibling_of_repo
Scenario: Fails for checked-out branch
- GIVEN the currently checked-out branch
- WHEN
create_worktree()is called for it - THEN it SHALL fail
Test: git_integration::create_worktree_fails_for_checked_out_branch
Requirement: Directory naming SHALL be correct in integration tests
Scenario: Project name from real repo path
- GIVEN a repo at
.../test-repo/ - WHEN
project_name()is called - THEN it SHALL return
"test-repo"
Test: git_integration::project_name_from_repo_path
Scenario: Worktree dir name replaces slashes
- WHEN
worktree_dir_name("my-project", "feature/auth-flow")is called - THEN it SHALL return
"my-project-feature-auth-flow"
Test: git_integration::worktree_dir_name_replaces_slashes
Scenario: Worktree dir name strips unsafe chars
- WHEN
worktree_dir_name("proj", "feat/special@chars!")is called - THEN
@and!SHALL be stripped
Test: git_integration::worktree_dir_name_strips_unsafe_chars
Scenario: Worktree dir name handles nested slashes
- WHEN
worktree_dir_name("proj", "feature/deep/nested/branch")is called - THEN it SHALL return
"proj-feature-deep-nested-branch"
Test: git_integration::worktree_dir_name_handles_nested_slashes
Tmux Orchestration
Purpose
Orchestrate tmux sessions with multiple panes, each running an AI CLI in a git worktree. Uses a builder pattern for testability and dry-run support, with configurable mouse mode and automatic tiled layout.
Requirements
Requirement: Check tmux availability with actionable error
The system SHALL verify tmux is installed on PATH and provide install instructions if missing.
Scenario: tmux is present on PATH
- GIVEN tmux is installed
- WHEN
ensure_tmux_installed()is called - THEN it SHALL return
Ok(())
Test: tmux::tests::ensure_tmux_installed_succeeds_when_present
Requirement: Create named sessions derived from project name
The system SHALL name tmux sessions as paw-<project_name>.
Scenario: Session named after project
- GIVEN project name
"my-project" - WHEN a session is built
- THEN the session name SHALL be
"paw-my-project"
Test: tmux::tests::session_is_named_after_project
Scenario: Session creation command uses correct name
- GIVEN project name
"app" - WHEN a session is built
- THEN the commands SHALL include
new-sessionwithpaw-app
Test: tmux::tests::session_creation_command_uses_session_name
Requirement: Session name override via builder
The builder SHALL support overriding the default paw-<project> session name with a custom name.
Scenario: Override replaces default name
- GIVEN
session_name("custom-session-name")is set on the builder - WHEN the session is built
- THEN the session name SHALL be
"custom-session-name"and commands SHALL target it
Test: tmux::tests::session_name_override_replaces_default
Requirement: Dynamic pane count matches input
The number of panes in the session SHALL match the number of PaneSpec entries added via the builder.
Scenario: Two panes created
- GIVEN 2 pane specs added
- WHEN the session is built
- THEN exactly 2
send-keyscommands SHALL be emitted
Test: tmux::tests::pane_count_matches_input_for_two_panes
Scenario: Five panes created
- GIVEN 5 pane specs added
- WHEN the session is built
- THEN exactly 5
send-keyscommands SHALL be emitted
Test: tmux::tests::pane_count_matches_input_for_five_panes
Scenario: Building with no panes is an error
- GIVEN no pane specs added
- WHEN
build()is called - THEN it SHALL return an error
Test: tmux::tests::building_with_no_panes_is_an_error
Requirement: Correct commands sent to each pane
Each pane SHALL receive a cd <worktree> && <cli_command> command targeting the correct pane index.
Scenario: Each pane receives cd and CLI command
- GIVEN two panes with different worktrees and CLIs
- WHEN the session is built
- THEN each
send-keyscommand SHALL containcd <worktree> && <cli>
Test: tmux::tests::each_pane_receives_cd_and_cli_command
Scenario: Commands are submitted with Enter
- GIVEN a pane spec
- WHEN the session is built
- THEN the
send-keyscommand SHALL includeEnter
Test: tmux::tests::pane_commands_are_submitted_with_enter
Scenario: Each pane targets a distinct index
- GIVEN 3 panes
- WHEN the session is built
- THEN
send-keysSHALL target:0.0,:0.1, and:0.2respectively
Test: tmux::tests::each_pane_targets_a_distinct_pane_index
Requirement: Pane titles show branch and CLI
Each pane SHALL be titled with <branch> → <cli_command> and border status configured.
Scenario: Pane titles contain branch and CLI
- GIVEN panes with branches and CLIs
- WHEN the session is built
- THEN
select-pane -Tcommands SHALL set titles like"feat/auth → claude"
Test: tmux::tests::each_pane_is_titled_with_branch_and_cli
Scenario: Pane border status configured
- GIVEN any session
- WHEN the session is built
- THEN
pane-border-statusSHALL be set totopandpane-border-formatSHALL use#{pane_title}
Test: tmux::tests::pane_border_status_is_configured
Requirement: Configurable mouse mode per session
Mouse mode SHALL be enabled by default and be disableable via the builder.
Scenario: Mouse mode enabled by default
- GIVEN no explicit mouse mode setting
- WHEN the session is built
- THEN a
mouse oncommand SHALL be emitted
Test: tmux::tests::mouse_mode_enabled_by_default
Scenario: Mouse mode can be disabled
- GIVEN
mouse_mode(false)is set on the builder - WHEN the session is built
- THEN no
mouse oncommand SHALL be emitted
Test: tmux::tests::mouse_mode_can_be_disabled
Requirement: Attach to a tmux session
The system SHALL attach the current terminal to a named tmux session, returning an error if the session does not exist.
Scenario: Attaching to a nonexistent session fails
- GIVEN no tmux session with the given name exists
- WHEN
attach()is called - THEN it SHALL return an error
Test: e2e_tests::attach_fails_for_nonexistent_session
Requirement: Session liveness check
The system SHALL check whether a tmux session is alive by name.
Scenario: Nonexistent session reports not alive
- GIVEN no tmux session with the queried name exists
- WHEN
is_session_alive()is called - THEN it SHALL return
false
Test: tmux::tests::is_session_alive_returns_false_for_nonexistent
Requirement: Session lifecycle management
The system SHALL support creating, checking, and killing tmux sessions.
Scenario: Full create-check-kill lifecycle
- GIVEN a tmux session is created
- WHEN
is_session_alive()is called, thenkill_session(), thenis_session_alive()again - THEN it SHALL be alive after creation and not alive after killing
Test: tmux::tests::session_lifecycle_create_check_kill
Scenario: Built session can be executed and killed
- GIVEN a session built via
TmuxSessionBuilder - WHEN
execute()is called - THEN the tmux session SHALL be alive, and after
kill_session()it SHALL be gone
Test: tmux::tests::built_session_can_be_executed_and_killed
Requirement: Session name collision resolution
The system SHALL resolve name collisions by appending -2, -3, etc. to the base session name.
Scenario: No collision returns base name
- GIVEN no existing session with the base name
- WHEN
resolve_session_name()is called - THEN it SHALL return
paw-<project_name>
Test: tmux::tests::resolve_session_name_returns_base_when_no_collision
Scenario: Collision appends numeric suffix
- GIVEN a session with the base name already exists
- WHEN
resolve_session_name()is called - THEN it SHALL return
paw-<project_name>-2
Test: tmux::tests::resolve_session_name_appends_suffix_on_collision
Requirement: Tmux session lifecycle SHALL work against a real tmux server
Scenario: Create and kill session lifecycle
- GIVEN a tmux session is created via the builder
- WHEN
execute(),is_session_alive(), andkill_session()are called - THEN the session SHALL be alive after creation and gone after killing
Test: e2e_tests::tmux_session_create_and_kill_lifecycle
Scenario: Five panes with different CLIs
- GIVEN 5 pane specs with different branch/CLI pairs
- WHEN the session is executed
- THEN tmux SHALL have 5 panes with correct titles
Test: e2e_tests::tmux_session_with_five_panes_and_different_clis
Scenario: Mouse mode enabled by default against live tmux
- GIVEN a session built with default settings
- WHEN
tmux show-optionis queried - THEN mouse SHALL be “on”
Test: e2e_tests::tmux_mouse_mode_enabled_by_default
Scenario: is_session_alive returns false for nonexistent (e2e)
- GIVEN no session with the queried name
- WHEN
is_session_alive()is called - THEN it SHALL return
false
Test: e2e_tests::tmux_is_session_alive_returns_false_for_nonexistent
Scenario: Attach succeeds for live session
- GIVEN a live tmux session
- WHEN
attach()is called and the client is detached programmatically - THEN the function SHALL execute without panic
Test: e2e_tests::attach_succeeds_for_live_session
Requirement: E2E commands SHALL behave correctly against real repos
Scenario: Dry run shows session plan
- GIVEN a git repo with branches and
--dry-run --cli echo --branches feat/a,feat/b - WHEN the binary runs
- THEN stdout SHALL contain “Dry run”, branch names, and the CLI name
Test: e2e_tests::dry_run_with_flags_shows_plan
Scenario: Preset not found returns error
- GIVEN a git repo with no presets configured
- WHEN
start --preset nonexistentis run - THEN it SHALL fail with stderr mentioning “not found”
Test: e2e_tests::preset_not_found_returns_error
Scenario: Stop with no session
- GIVEN a git repo with no active session
- WHEN
stopis run - THEN it SHALL succeed with stdout mentioning “No active session”
Test: e2e_tests::stop_with_no_session
Scenario: Purge with no session
- GIVEN a git repo with no active session
- WHEN
purge --forceis run - THEN it SHALL succeed with stdout mentioning “No session to purge”
Test: e2e_tests::purge_with_no_session
Scenario: Status with no session
- GIVEN a git repo with no active session
- WHEN
statusis run - THEN it SHALL succeed with stdout mentioning “No session”
Test: e2e_tests::status_with_no_session
Scenario: Stop from non-git directory fails
- GIVEN a directory that is not a git repository
- WHEN
stopis run - THEN it SHALL fail with “Not a git repository”
Test: e2e_tests::stop_from_non_git_dir_fails
Scenario: Status from non-git directory fails
- GIVEN a directory that is not a git repository
- WHEN
statusis run - THEN it SHALL fail with “Not a git repository”
Test: e2e_tests::status_from_non_git_dir_fails
Session State
Purpose
Persist session state to disk for recovery after crashes, reboots, or manual stops. Stores one JSON file per session under the XDG data directory, with atomic writes and tmux liveness checks.
Requirements
Requirement: Save session state atomically
The system SHALL serialize session data to JSON and write it atomically using a temp file and rename to prevent corruption.
Scenario: Saved session round-trips with all fields intact
- GIVEN an active session with 3 worktrees
- WHEN
save_session()is called and the session is loaded back - THEN all fields (session_name, repo_path, project_name, created_at, status, worktrees) SHALL match the original
Test: session::tests::saved_session_can_be_loaded_with_all_fields_intact
Scenario: Saving again replaces previous state
- GIVEN a previously saved session
- WHEN
save_session()is called with updated fields - THEN the new state SHALL overwrite the old state
Test: session::tests::saving_again_replaces_previous_state
Requirement: Load session by name
The system SHALL load a session from disk by name, returning None if the file does not exist.
Scenario: Loading a nonexistent session returns None
- GIVEN no session file exists with the given name
- WHEN
load_session()is called - THEN it SHALL return
Ok(None)
Test: session::tests::loading_nonexistent_session_returns_none
Requirement: Find session by repository path
The system SHALL scan all session files and return the session matching a given repository path.
Scenario: Finds correct session among multiple
- GIVEN two sessions for different repositories
- WHEN
find_session_for_repo()is called with one repo path - THEN it SHALL return the matching session
Test: session::tests::finds_correct_session_among_multiple_by_repo_path
Scenario: No matching session
- GIVEN saved sessions for other repositories
- WHEN
find_session_for_repo()is called with a different path - THEN it SHALL return
None
Test: session::tests::find_returns_none_when_no_repo_matches
Scenario: No sessions directory
- GIVEN no sessions directory exists
- WHEN
find_session_for_repo()is called - THEN it SHALL return
None
Test: session::tests::find_returns_none_when_no_sessions_exist
Requirement: Delete session by name
The system SHALL delete a session file, succeeding even if the file does not exist (idempotent).
Scenario: Deleted session is no longer loadable
- GIVEN a saved session
- WHEN
delete_session()is called - THEN
load_session()SHALL returnNone
Test: session::tests::deleted_session_is_no_longer_loadable
Scenario: Deleting nonexistent session succeeds
- GIVEN no session file with the given name
- WHEN
delete_session()is called - THEN it SHALL return
Ok(())
Test: session::tests::deleting_nonexistent_session_succeeds
Requirement: Effective status combines file state with tmux liveness
The system SHALL report Stopped when the file says Active but the tmux session is dead.
Scenario: Active file and alive tmux means Active
- GIVEN a session with
status = Activeand tmux is alive - WHEN
effective_status()is called - THEN it SHALL return
Active
Test: session::tests::file_says_active_and_tmux_alive_means_active
Scenario: Active file but dead tmux means Stopped
- GIVEN a session with
status = Activeand tmux is dead - WHEN
effective_status()is called - THEN it SHALL return
Stopped
Test: session::tests::file_says_active_but_tmux_dead_means_stopped
Scenario: Stopped file stays Stopped
- GIVEN a session with
status = Stopped - WHEN
effective_status()is called regardless of tmux state - THEN it SHALL return
Stopped
Test: session::tests::file_says_stopped_stays_stopped_regardless_of_tmux
Requirement: SessionStatus display format
The SessionStatus enum SHALL display as lowercase strings.
Scenario: SessionStatus display strings
- GIVEN
SessionStatus::ActiveandSessionStatus::Stopped - WHEN formatted with
Display - THEN they SHALL render as
"active"and"stopped"
Test: session::tests::session_status_displays_as_lowercase_string
Requirement: Recovery data survives tmux crashes
After a tmux crash, the persisted session SHALL contain all data needed to reconstruct the session.
Scenario: Crashed session has all recovery data
- GIVEN a saved session with worktrees
- WHEN tmux crashes and the session is loaded from disk
- THEN it SHALL have the session name, repo path, and all worktree details (branch, path, CLI)
Test: session::tests::recovery_after_tmux_crash_has_all_data_to_reconstruct
Requirement: Session persistence SHALL work through the public API
Scenario: Save and load round-trip
- GIVEN a session with 2 worktrees
- WHEN
save_session_in()andload_session_from()are called - THEN all fields SHALL match
Test: session_integration::save_and_load_round_trip
Scenario: Find session by repo path
- GIVEN a saved session
- WHEN
find_session_for_repo_in()is called with the matching repo path - THEN the correct session SHALL be returned
Test: session_integration::find_session_by_repo_path
Scenario: Find returns None for unknown repo
- GIVEN no matching session
- WHEN
find_session_for_repo_in()is called - THEN it SHALL return
None
Test: session_integration::find_session_returns_none_for_unknown_repo
Scenario: Find correct session among multiple
- GIVEN two sessions for different repos
- WHEN
find_session_for_repo_in()is called for one - THEN the correct session SHALL be returned
Test: session_integration::find_correct_session_among_multiple
Scenario: Delete removes session
- GIVEN a saved session
- WHEN
delete_session_in()is called - THEN
load_session_from()SHALL returnNone
Test: session_integration::delete_removes_session
Scenario: Delete nonexistent is idempotent
- GIVEN no session file
- WHEN
delete_session_in()is called - THEN it SHALL succeed
Test: session_integration::delete_nonexistent_is_idempotent
Scenario: Load nonexistent returns None
- GIVEN no session file
- WHEN
load_session_from()is called - THEN it SHALL return
None
Test: session_integration::load_nonexistent_returns_none
Scenario: Saving again replaces previous state
- GIVEN a saved session
- WHEN the status is changed and saved again
- THEN the loaded session SHALL have the new status
Test: session_integration::saving_again_replaces_previous_state
Scenario: Effective status active when tmux alive
- GIVEN a session with
Activestatus and tmux alive - WHEN
effective_status()is called - THEN it SHALL return
Active
Test: session_integration::effective_status_active_when_tmux_alive
Scenario: Effective status stopped when tmux dead
- GIVEN a session with
Activestatus and tmux dead - WHEN
effective_status()is called - THEN it SHALL return
Stopped
Test: session_integration::effective_status_stopped_when_tmux_dead
Scenario: Effective status stopped stays stopped
- GIVEN a session with
Stoppedstatus - WHEN
effective_status()is called - THEN it SHALL return
Stoppedregardless of tmux
Test: session_integration::effective_status_stopped_stays_stopped
Scenario: Saved session has all recovery fields
- GIVEN a saved and reloaded session
- WHEN recovery fields are checked
- THEN session_name, repo_path, project_name, and all worktree entries SHALL be non-empty
Test: session_integration::saved_session_has_all_recovery_fields
Configuration
Purpose
Parse TOML configuration from global (~/.config/git-paw/config.toml) and per-repo (.git-paw/config.toml) files. Supports custom CLI definitions, presets, and programmatic add/remove of custom CLIs with repo config overriding global config.
Requirements
Requirement: Parse TOML config with all fields
The system SHALL parse a TOML configuration file containing default_cli, mouse, clis, and presets fields.
Scenario: Config with all fields populated
- GIVEN a TOML file with
default_cli,mouse, custom CLIs, and presets - WHEN the file is loaded
- THEN all fields SHALL be correctly parsed
Test: config::tests::parses_config_with_all_fields
Scenario: All fields are optional
- GIVEN a TOML file with only
default_cli - WHEN the file is loaded
- THEN missing fields SHALL default to
Noneor empty collections
Test: config::tests::all_fields_are_optional
Scenario: No config files exist
- GIVEN neither global nor repo config files exist
- WHEN
load_config()is called - THEN it SHALL return a default config with all fields empty/None
Test: config::tests::returns_defaults_when_no_files_exist
Scenario: Invalid TOML reports error with file path
- GIVEN a malformed TOML file
- WHEN it is loaded
- THEN the error message SHALL include the file path
Test: config::tests::reports_error_for_invalid_toml
Requirement: Merge repo config over global config
The system SHALL merge per-repo configuration on top of global configuration, with repo values taking precedence for scalar fields and map entries.
Scenario: Repo overrides global scalar fields
- GIVEN global config has
default_cli = "claude"andmouse = true, and repo hasdefault_cli = "gemini" - WHEN configs are merged
- THEN
default_cliSHALL be"gemini"andmouseSHALL betrue(preserved from global)
Test: config::tests::repo_config_overrides_global_scalars
Scenario: CLI maps are merged
- GIVEN global config has CLI
agent-aand repo config has CLIagent-b - WHEN configs are merged
- THEN both CLIs SHALL be present
Test: config::tests::repo_config_merges_cli_maps
Scenario: Repo CLI overrides global CLI with same name
- GIVEN both global and repo define a CLI named
my-agent - WHEN configs are merged
- THEN the repo definition SHALL win
Test: config::tests::repo_cli_overrides_global_cli_with_same_name
Scenario: Only global config exists
- GIVEN a global config file but no repo config
- WHEN
load_config()is called - THEN global values SHALL be used
Test: config::tests::load_config_from_reads_global_file_when_no_repo
Scenario: Only repo config exists
- GIVEN a repo config file but no global config
- WHEN
load_config()is called - THEN repo values SHALL be used
Test: config::tests::load_config_from_reads_repo_file_when_no_global
Requirement: Preset lookup by name
The system SHALL provide access to named presets that define branches and a CLI.
Scenario: Preset accessible by name
- GIVEN a config with a preset named
"backend" - WHEN
get_preset("backend")is called - THEN it SHALL return the preset with its branches and CLI
Test: config::tests::preset_accessible_by_name
Scenario: Missing preset returns None
- GIVEN a config without the requested preset
- WHEN
get_preset("nonexistent")is called - THEN it SHALL return
None
Test: config::tests::preset_returns_none_when_not_in_config
Requirement: Add custom CLIs to global config
The system SHALL add custom CLI definitions to the global config, resolving non-absolute commands via PATH.
Scenario: Add CLI with absolute path
- GIVEN an absolute path to a CLI binary
- WHEN
add_custom_cli()is called - THEN the CLI SHALL be written to the config file
Test: config::tests::add_cli_writes_to_config_file
Scenario: Adding preserves existing entries
- GIVEN an existing CLI in the config
- WHEN a second CLI is added
- THEN both CLIs SHALL be present
Test: config::tests::add_cli_preserves_existing_entries
Scenario: Adding CLI with missing command fails
- GIVEN a command that does not exist on PATH
- WHEN
add_custom_cli()is called - THEN it SHALL return an error mentioning “not found on PATH”
Test: config::tests::add_cli_errors_when_command_not_on_path
Requirement: Remove custom CLIs from global config
The system SHALL remove a custom CLI by name, returning an error if the CLI is not found.
Scenario: Remove existing CLI
- GIVEN a config with CLIs
keep-meandremove-me - WHEN
remove_custom_cli("remove-me")is called - THEN only
keep-meSHALL remain
Test: config::tests::remove_cli_deletes_entry_from_config_file
Scenario: Remove nonexistent CLI returns error
- GIVEN a config without the named CLI
- WHEN
remove_custom_cli()is called - THEN it SHALL return
PawError::CliNotFound
Test: config::tests::remove_nonexistent_cli_returns_cli_not_found_error
Scenario: Remove CLI from empty/missing config returns error
- GIVEN no config file exists
- WHEN
remove_custom_cli()is called - THEN it SHALL return
PawError::CliNotFound
Test: config::tests::remove_cli_from_empty_config_returns_error
Requirement: Config survives round-trip serialization
A PawConfig SHALL be identical after save and reload.
Scenario: Config round-trip
- GIVEN a fully populated config
- WHEN saved and loaded back
- THEN it SHALL be equal to the original
Test: config::tests::config_survives_save_and_load
Requirement: Config loading SHALL work with real files
Scenario: Defaults when no files exist
- GIVEN a temp directory with no config files
- WHEN
load_config()is called - THEN all fields SHALL be None/empty
Test: config_integration::load_config_returns_defaults_when_no_files_exist
Scenario: Reads repo .git-paw/config.toml
- GIVEN a
.git-paw/config.tomlwith default_cli and mouse - WHEN
load_config()is called - THEN the values SHALL be read correctly
Test: config_integration::load_config_reads_repo_config
Scenario: Repo config with custom CLIs
- GIVEN a
.git-paw/config.tomlwith two custom CLIs - WHEN
load_config()is called - THEN both CLIs SHALL be parsed with correct fields
Test: config_integration::repo_config_with_custom_clis
Scenario: Repo config with presets
- GIVEN a
.git-paw/config.tomlwith two presets - WHEN
load_config()is called - THEN presets SHALL be accessible with correct branches and CLI
Test: config_integration::repo_config_with_presets
Scenario: Default PawConfig has no presets
- GIVEN a default
PawConfig - WHEN
get_preset("nonexistent")is called - THEN it SHALL return
None
Test: config_integration::get_preset_returns_none_for_unknown
Scenario: Repo config overrides default fields
- GIVEN a
.git-paw/config.tomlwith specific values - WHEN
load_config()is called - THEN the repo values SHALL take precedence
Test: config_integration::repo_config_overrides_default_fields
Scenario: Repo config path is correct
- GIVEN a temp directory
- WHEN
repo_config_path()is called - THEN it SHALL return
<dir>/.git-paw/config.toml
Test: config_integration::repo_config_path_is_in_repo_root
Scenario: Malformed TOML returns error
- GIVEN a
.git-paw/config.tomlwith invalid TOML - WHEN
load_config()is called - THEN it SHALL return an error
Test: config_integration::malformed_toml_returns_error
Scenario: Empty config file is valid
- GIVEN an empty
.git-paw/config.toml - WHEN
load_config()is called - THEN it SHALL return a default config
Test: config_integration::empty_config_file_is_valid
Requirement: Custom CLI management SHALL persist through file I/O
Scenario: Add CLI with absolute path
- GIVEN no config file
- WHEN
add_custom_cli_to()is called with an absolute path - THEN the CLI SHALL be persisted and reloadable
Test: config_integration::add_custom_cli_with_absolute_path
Scenario: Add CLI with display name
- GIVEN no config file
- WHEN
add_custom_cli_to()is called with a display name - THEN the display name SHALL be persisted
Test: config_integration::add_custom_cli_with_display_name
Scenario: Multiple CLIs preserved across adds
- GIVEN 4 CLIs added sequentially
- WHEN the config is loaded
- THEN all 4 SHALL be present with correct fields
Test: config_integration::add_multiple_custom_clis_preserves_all
Scenario: Adding overwrites existing entry
- GIVEN a CLI with name
my-agentalready exists - WHEN
add_custom_cli_to()is called with the same name but different values - THEN the new values SHALL replace the old
Test: config_integration::add_cli_overwrites_existing_entry
Scenario: Add CLI with nonexistent command fails
- GIVEN a non-absolute command that is not on PATH
- WHEN
add_custom_cli_to()is called - THEN it SHALL return an error
Test: config_integration::add_cli_with_nonexistent_path_command_fails
Scenario: Remove custom CLI
- GIVEN two CLIs in the config
- WHEN one is removed
- THEN only the other SHALL remain
Test: config_integration::remove_custom_cli
Scenario: Remove nonexistent CLI returns error
- GIVEN no CLIs in the config
- WHEN
remove_custom_cli_from()is called - THEN it SHALL return an error
Test: config_integration::remove_nonexistent_cli_returns_error
Scenario: Remove all CLIs leaves empty config
- GIVEN one CLI in the config
- WHEN it is removed
- THEN the CLI map SHALL be empty
Test: config_integration::remove_all_custom_clis_leaves_empty_config
Requirement: Global and repo config SHALL merge custom CLIs correctly
Scenario: Repo custom CLIs merge with global
- GIVEN global config with 2 CLIs and repo config with 2 CLIs (one overlapping)
- WHEN
load_config_from()is called - THEN the result SHALL have 3 CLIs, with repo winning on collision
Test: config_integration::repo_custom_clis_merge_with_global_custom_clis
Requirement: Config SHALL handle many custom CLIs
Scenario: Config with 10 custom CLIs
- GIVEN a config file with 10 custom CLI definitions
- WHEN
load_config()is called - THEN all 10 SHALL be parsed correctly
Test: config_integration::config_with_many_custom_clis
Interactive Selection
Purpose
Interactive selection prompts for choosing branches and AI CLIs. Supports uniform (same CLI for all branches) and per-branch assignment modes, with CLI flags that skip prompts. Logic is separated from UI via the Prompter trait for testability.
Requirements
Requirement: CLI flags skip all prompts when both provided
When both --cli and --branches flags are provided, the system SHALL skip all interactive prompts and map the CLI to all specified branches.
Scenario: Both flags skip all prompts
- GIVEN
--cli alphaand--branches feature/auth,fix/apiflags - WHEN
run_selection()is called - THEN it SHALL return mappings without invoking any prompts
Test: interactive::tests::both_flags_skips_all_prompts_and_maps_cli_to_all_branches
Requirement: CLI flag skips CLI prompt but prompts for branches
When only --cli is provided, the system SHALL prompt for branch selection but skip CLI selection.
Scenario: CLI flag provided, branches prompted
- GIVEN
--cli alphaflag and no branches flag - WHEN
run_selection()is called - THEN branch selection SHALL be prompted and the flag CLI SHALL be used
Test: interactive::tests::cli_flag_skips_cli_prompt_but_prompts_for_branches
Requirement: Branches flag skips branch prompt but prompts for CLI
When only --branches is provided, the system SHALL skip branch selection but prompt for CLI assignment.
Scenario: Branches flag provided, CLI prompted in uniform mode
- GIVEN
--branchesflag and no CLI flag - WHEN user selects uniform mode
- THEN the selected CLI SHALL be mapped to all flagged branches
Test: interactive::tests::branches_flag_skips_branch_prompt_but_prompts_for_cli_uniform
Requirement: Uniform mode maps same CLI to all branches
In uniform mode, the system SHALL assign the selected CLI to every selected branch.
Scenario: Uniform mode selection
- GIVEN user selects uniform mode, picks 2 branches and 1 CLI
- WHEN
run_selection()completes - THEN both branches SHALL be mapped to the same CLI
Test: interactive::tests::uniform_mode_maps_same_cli_to_all_selected_branches
Requirement: Per-branch mode maps different CLIs to each branch
In per-branch mode, the system SHALL prompt for a CLI for each selected branch individually.
Scenario: Per-branch mode selection
- GIVEN user selects per-branch mode with 2 branches
- WHEN different CLIs are chosen for each branch
- THEN each branch SHALL be mapped to its respective CLI
Test: interactive::tests::per_branch_mode_maps_different_cli_to_each_branch
Scenario: Per-branch mode with branches flag
- GIVEN branches provided via flag and per-branch mode selected
- WHEN different CLIs are chosen
- THEN each flagged branch SHALL be mapped to its selected CLI
Test: interactive::tests::per_branch_mode_with_branches_flag
Requirement: Error when no CLIs available
The system SHALL return PawError::NoCLIsFound when the CLI list is empty.
Scenario: Empty CLI list
- GIVEN no CLIs available
- WHEN
run_selection()is called - THEN it SHALL return
Err(PawError::NoCLIsFound)
Test: interactive::tests::no_clis_available_returns_error
Requirement: Error when no branches available
The system SHALL return PawError::BranchError when the branch list is empty.
Scenario: Empty branch list
- GIVEN no branches available
- WHEN
run_selection()is called - THEN it SHALL return
Err(PawError::BranchError)
Test: interactive::tests::no_branches_available_returns_error
Requirement: User cancellation propagates as PawError::UserCancelled
The system SHALL propagate cancellation (Ctrl+C or empty selection) as PawError::UserCancelled.
Scenario: User cancels branch selection
- GIVEN user presses Ctrl+C during branch selection
- WHEN
run_selection()is called - THEN it SHALL return
Err(PawError::UserCancelled)
Test: interactive::tests::user_cancels_branch_selection_returns_cancelled
Scenario: User selects no branches
- GIVEN user confirms with zero branches selected
- WHEN
run_selection()is called - THEN it SHALL return
Err(PawError::UserCancelled)
Test: interactive::tests::user_selects_no_branches_returns_cancelled
Scenario: User cancels CLI selection
- GIVEN user presses Ctrl+C during CLI selection
- WHEN
run_selection()is called - THEN it SHALL return
Err(PawError::UserCancelled)
Test: interactive::tests::user_cancels_cli_selection_returns_cancelled
Requirement: Subset branch selection
The system SHALL support selecting a subset of available branches.
Scenario: Selecting one of two branches
- GIVEN 2 available branches
- WHEN user selects only the second
- THEN only that branch SHALL appear in the result
Test: interactive::tests::selecting_subset_of_branches_works
Requirement: CliMode display format
The CliMode enum SHALL display as human-readable descriptions.
Scenario: CliMode display strings
- GIVEN
CliMode::UniformandCliMode::PerBranch - WHEN formatted with
Display - THEN they SHALL render as
"Same CLI for all branches"and"Different CLI per branch"
Test: interactive::tests::cli_mode_display
Requirement: CliInfo display format
CliInfo SHALL display as the binary name when it matches the display name, or as "DisplayName (binary)" when they differ.
Scenario: Same display and binary name
- GIVEN a
CliInfowheredisplay_nameequalsbinary_name - WHEN formatted with
Display - THEN it SHALL render as just the binary name
Test: interactive::tests::cli_info_display_same_names
Scenario: Different display and binary name
- GIVEN a
CliInfowheredisplay_namediffers frombinary_name - WHEN formatted with
Display - THEN it SHALL render as
"DisplayName (binary_name)"
Test: interactive::tests::cli_info_display_different_names
Error Handling
Purpose
Define the central error type PawError used across all git-paw modules. Every variant carries an actionable, user-facing message and maps to a process exit code.
Requirements
Requirement: Actionable error messages for each variant
Each PawError variant SHALL produce a user-facing message that explains the problem and suggests a remedy where appropriate.
Scenario: NotAGitRepo is actionable
- GIVEN
PawError::NotAGitRepo - WHEN formatted with
Display - THEN the message SHALL mention “git repository” and name the tool
Test: error::tests::test_not_a_git_repo_is_actionable
Scenario: TmuxNotInstalled includes install instructions
- GIVEN
PawError::TmuxNotInstalled - WHEN formatted with
Display - THEN the message SHALL include both
brew installandapt installhints
Test: error::tests::test_tmux_not_installed_includes_install_instructions
Scenario: NoCLIsFound suggests add-cli
- GIVEN
PawError::NoCLIsFound - WHEN formatted with
Display - THEN the message SHALL suggest the
add-clicommand
Test: error::tests::test_no_clis_found_suggests_add_cli
Scenario: WorktreeError includes detail
- GIVEN
PawError::WorktreeError("failed to create") - WHEN formatted with
Display - THEN the message SHALL include the inner detail string
Test: error::tests::test_worktree_error_includes_detail
Scenario: SessionError includes detail
- GIVEN
PawError::SessionError("file corrupt") - WHEN formatted with
Display - THEN the message SHALL include the inner detail string
Test: error::tests::test_session_error_includes_detail
Scenario: ConfigError includes detail
- GIVEN
PawError::ConfigError("invalid toml") - WHEN formatted with
Display - THEN the message SHALL include the inner detail string
Test: error::tests::test_config_error_includes_detail
Scenario: BranchError includes detail
- GIVEN
PawError::BranchError("not found") - WHEN formatted with
Display - THEN the message SHALL include the inner detail string
Test: error::tests::test_branch_error_includes_detail
Scenario: UserCancelled has a message
- GIVEN
PawError::UserCancelled - WHEN formatted with
Display - THEN the message SHALL not be empty
Test: error::tests::test_user_cancelled_is_not_empty
Scenario: TmuxError includes detail
- GIVEN
PawError::TmuxError("session failed") - WHEN formatted with
Display - THEN the message SHALL include the inner detail string
Test: error::tests::test_tmux_error_includes_detail
Scenario: CliNotFound includes CLI name
- GIVEN
PawError::CliNotFound("my-agent") - WHEN formatted with
Display - THEN the message SHALL include the missing CLI name
Test: error::tests::test_cli_not_found_includes_cli_name
Requirement: Exit codes distinguish cancellation from errors
UserCancelled SHALL exit with code 2; all other errors SHALL exit with code 1.
Scenario: UserCancelled exit code
- GIVEN
PawError::UserCancelled - WHEN
exit_code()is called - THEN it SHALL return
2
Test: error::tests::test_user_cancelled_exit_code
Scenario: General errors exit code
- GIVEN any non-cancellation error variant
- WHEN
exit_code()is called - THEN it SHALL return
1
Test: error::tests::test_general_errors_exit_code
Requirement: Exit method prints to stderr and exits with correct code
PawError::exit() SHALL print the error message to stderr and terminate with the appropriate exit code.
Scenario: NotAGitRepo exits with code 1
- GIVEN the binary is run outside a git repository
- WHEN the error propagates to
exit() - THEN the process SHALL exit with code 1 and stderr SHALL contain the error message
Test: e2e_tests::error_exit_code_is_1_for_not_a_git_repo
Scenario: ConfigError exits with code 1
- GIVEN a nonexistent preset is requested
- WHEN the error propagates to
exit() - THEN the process SHALL exit with code 1 and stderr SHALL mention “not found”
Test: e2e_tests::error_exit_code_is_1_for_preset_not_found
Requirement: Debug representation is derivable
All PawError variants SHALL support Debug formatting.
Scenario: Debug format includes variant name
- GIVEN
PawError::NotAGitRepo - WHEN formatted with
Debug - THEN the output SHALL contain
"NotAGitRepo"
Test: error::tests::test_debug_derived
Contributing
Contributions to git-paw are welcome! This guide covers the development workflow.
Prerequisites
- Rust (see
rust-toolchain.tomlfor the exact version) - tmux
- just (task runner)
Getting Started
git clone https://github.com/bearicorn/git-paw.git
cd git-paw
just check
Development Commands
git-paw uses just as a task runner. Key recipes:
| Command | Description |
|---|---|
just check | Run fmt + clippy + tests |
just test | Run all tests |
just test-all | Run all tests including tmux-dependent ones |
just lint | Run cargo fmt --check and cargo clippy |
just coverage | Generate HTML coverage report |
just docs | Build and open mdBook documentation |
just api-docs | Build and open Rustdoc API docs |
just build | Build release binary |
just install | Install from local source |
just clean | Clean build artifacts |
Building
# Debug build
cargo build
# Release build
cargo build --release
# Install locally
cargo install --path .
Testing
# Unit tests
cargo test
# Include tmux-dependent tests (requires tmux installed)
cargo test -- --include-ignored
# Coverage report
cargo llvm-cov --html
# Open: target/llvm-cov/html/index.html
Tests are organized as:
- Unit tests —
#[cfg(test)]modules within each source file - Integration tests —
tests/directory (CLI binary tests, worktree lifecycle, session round-trips) - Tmux-dependent tests — marked
#[ignore], run with--include-ignored
Code Style
- Formatting:
cargo fmt(config inrustfmt.toml) - Linting:
cargo clippy -- -D warningswith pedantic lints enabled - No panics: No
unwrap()orexpect()in non-testsrc/code - Documentation:
//!module-level doc comments,///on all public items
Commit Format
This project uses Conventional Commits:
<type>(<scope>): <description>
[optional body]
[optional footer(s)]
Types: feat, fix, docs, style, refactor, test, ci, chore
Scopes: cli, detect, git, tmux, session, config, interactive, error
Examples:
feat(tmux): add mouse mode support
fix(session): handle missing state file gracefully
docs: update installation instructions
test(git): add worktree creation edge cases
Branch Naming
feat/<description> # New features
fix/<description> # Bug fixes
docs/<description> # Documentation
test/<description> # Test additions
ci/<description> # CI/CD changes
Pull Request Process
- Fork the repository
- Create a feature branch from
main - Make your changes
- Ensure
just checkpasses (fmt, clippy, tests) - Write or update tests as needed
- Open a PR against
main
PRs should:
- Have a clear title and description
- Pass all CI checks
- Include tests for new functionality
- Follow the commit format above
Architecture
See the Architecture chapter for an overview of the module structure and design decisions.
Changelog
All notable changes to this project will be documented here.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
Added
- Project scaffolding with clap v4 CLI entry point
- AI CLI auto-detection (claude, codex, gemini, aider, vibe, qwen, amp)
- Git worktree creation and management
- Tmux session orchestration with builder pattern
- Session state persistence with crash recovery
- TOML configuration (global and per-repo)
- Interactive selection prompts (mode, branch, CLI pickers)
- Error handling with actionable messages and exit codes
- Custom CLI registration (
add-cli/remove-cli) - Preset support for one-command launch
- Mouse mode for tmux sessions
- Pane titles showing branch and CLI names
- Dry-run mode (
--dry-run) - Non-interactive mode (
--cliand--branchesflags) - mdBook documentation site
FAQ
General
What does “paw” stand for?
Parallel AI Worktrees.
Does git-paw work on Windows?
Only through WSL (Windows Subsystem for Linux). git-paw requires tmux, which is not natively available on Windows. See the Installation chapter for WSL setup instructions.
Do I need tmux experience to use git-paw?
No. git-paw creates and manages tmux sessions for you. Mouse mode is enabled by default, so you can click to switch panes and drag to resize. The only tmux shortcut you might need is Ctrl-b d to detach.
Can I use git-paw with AI CLIs not in the supported list?
Yes! Use git paw add-cli to register any CLI binary. See Custom CLIs.
Sessions
What happens if I close my terminal?
The tmux session keeps running in the background. Run git paw again to reattach.
What happens if tmux crashes or my machine reboots?
git-paw saves session state to disk. The next time you run git paw, it detects the saved state and automatically recovers: reuses existing worktrees, recreates the tmux session, and relaunches your AI CLIs.
Can I run multiple git-paw sessions?
One session per repository. To work with multiple repos, open separate terminals and run git paw in each repo directory.
How do I switch between branches in a session?
Click the pane you want (mouse mode is on by default), or use Ctrl-b followed by arrow keys to navigate between panes. Each pane is labeled with its branch and CLI in the border title.
Worktrees
What are git worktrees?
Git worktrees let you check out multiple branches simultaneously in separate directories. Each worktree is a fully functional working copy of the repository sharing the same .git data. Changes in one worktree don’t affect others.
Where does git-paw create worktrees?
As siblings of your main repo directory. For a project at ~/projects/my-app with branch feat/auth:
~/projects/my-app/ ← your repo
~/projects/my-app-feat-auth/ ← worktree created by git-paw
Does stopping a session delete my worktrees?
No. git paw stop kills the tmux session but keeps worktrees and any uncommitted work intact. Only git paw purge removes worktrees.
Can I manually work in a git-paw worktree?
Yes. Worktrees are regular git working directories. You can cd into them, edit files, commit, push — anything you’d do in a normal repo. When you restart the session, git-paw reuses the existing worktrees.
Configuration
Where are config files stored?
| Level | Path |
|---|---|
| Global | ~/.config/git-paw/config.toml |
| Per-repo | .git-paw/config.toml (in repo root) |
Both are optional. See Configuration.
How do I set a default CLI?
Add to your global or repo config:
default_cli = "claude"
How do I disable mouse mode?
mouse = false
This only affects git-paw’s tmux sessions, not your other tmux usage.
Troubleshooting
“Not a git repository”
Run git-paw from inside a git repository. It needs to be anywhere within a repo’s working tree.
“tmux is required but not installed”
Install tmux:
- macOS:
brew install tmux - Ubuntu/Debian:
sudo apt install tmux - Fedora:
sudo dnf install tmux
“No AI CLIs found on PATH”
Install at least one AI coding CLI (see Supported AI CLIs), or register a custom one:
git paw add-cli my-tool /path/to/my-tool
“no space for new pane” in tmux
This can happen with many branches on a small terminal. Make your terminal window larger before launching, or select fewer branches. git-paw applies tiled layout progressively to minimize this issue.
Session state seems stale
git-paw checks tmux liveness to determine effective status. If something seems off, try:
git paw purge --force
git paw start
Third-Party Licenses
git-paw includes the following third-party software.
Apache License 2.0 (Apache-2.0)
- sdd 3.0.10
License text
Apache License
Version 2.0, April 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2024-present Changgyoo Park
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
MIT License (MIT)
- cfg-if 1.0.4
- wait-timeout 0.2.1
License text
Copyright (c) 2014 Alex Crichton
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
Unicode License v3 (Unicode-3.0)
- unicode-ident 1.0.24
License text
UNICODE LICENSE V3
COPYRIGHT AND PERMISSION NOTICE
Copyright © 1991-2023 Unicode, Inc.
NOTICE TO USER: Carefully read the following legal agreement. BY
DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
Permission is hereby granted, free of charge, to any person obtaining a
copy of data files and any associated documentation (the "Data Files") or
software and any associated documentation (the "Software") to deal in the
Data Files or Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, and/or sell
copies of the Data Files or Software, and to permit persons to whom the
Data Files or Software are furnished to do so, provided that either (a)
this copyright and permission notice appear with all copies of the Data
Files or Software, or (b) this copyright and permission notice appear in
associated Documentation.
THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
THIRD PARTY RIGHTS.
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
FILES OR SOFTWARE.
Except as contained in this notice, the name of a copyright holder shall
not be used in advertising or otherwise to promote the sale, use or other
dealings in these Data Files or Software without prior written
authorization of the copyright holder.