git-paw 0.2.0

Parallel AI Worktrees — orchestrate multiple AI coding CLI sessions across git worktrees
Documentation
## 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 return `None`

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 = Active` and 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 = Active` and 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::Active` and `SessionStatus::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()` and `load_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 return `None`

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 `Active` status 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 `Active` status 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 `Stopped` status
- **WHEN** `effective_status()` is called
- **THEN** it SHALL return `Stopped` regardless 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`