--- phase: 01-core-infrastructure plan: 03 type: execute wave: 2 depends_on: - 01-01 - 01-02 files_modified: - src/gsd_openclaw/tmux_session.py - tests/conftest.py - tests/test_tmux_session.py autonomous: true requirements: - SESS-01 - SESS-02 - SESS-03 - SESS-07 user_setup: [] must_haves: truths: - "User can create a new tmux session for a project" - "User can list all active gsd sessions with their project associations" - "User can destroy a session when project is complete" - "Session names follow gsd-{project} format with collision handling" - "Multiple sessions can run concurrently without interference" artifacts: - path: "src/gsd_openclaw/tmux_session.py" provides: "tmux session lifecycle management" exports: ["TmuxSessionManager", "TmuxSession"] min_lines: 120 - path: "tests/test_tmux_session.py" provides: "Test coverage for tmux operations" contains: "test_create_session" key_links: - from: "src/gsd_openclaw/tmux_session.py" to: "libtmux.Server" via: "server = libtmux.Server()" pattern: "libtmux\\.Server" - from: "TmuxSessionManager" to: "state_manager.StateManager" via: "state persistence after operations" pattern: "StateManager" --- Implement tmux session lifecycle management using libtmux. Purpose: Core functionality for creating, listing, and destroying tmux sessions. This is the primary interface to tmux that all other components will use. Output: Working tmux_session.py with create/list/destroy operations using libtmux. @/home/starmen1265/.claude/get-shit-done/workflows/execute-plan.md @/home/starmen1265/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/01-core-infrastructure/01-CONTEXT.md @.planning/phases/01-core-infrastructure/01-RESEARCH.md **SessionState dataclass:** ```python @dataclass class SessionState: schema_version: str = "1.0" session_id: str = "" project_name: str = "" tmux_session_name: str = "" tmux_pane_id: str = "" created_at: str = "" last_active_at: str = "" command_count: int = 0 last_output_snapshot: Optional[str] = None display_name: Optional[str] = None ``` **StateManager class:** ```python class StateManager: def save(self, state: SessionState) -> None: ... def load(self) -> Optional[SessionState]: ... ``` **WorkspaceManager class:** ```python class WorkspaceManager: def create(self) -> Workspace: ... def get_workspace(self) -> Workspace: ... ``` **libtmux API:** ```python import libtmux server = libtmux.Server() # Create session session = server.new_session( session_name='gsd-my-project', attach=False, window_name='main' ) # List sessions for s in server.sessions: print(f"{s.name}: {s.session_id}") # Find session session = server.sessions.get(session_name='gsd-my-project') # Kill session session.kill() ``` Task 1: Implement TmuxSession dataclass and basic libtmux wrapper src/gsd_openclaw/tmux_session.py, tests/conftest.py, tests/test_tmux_session.py - Test 1: TmuxSession stores session metadata correctly - Test 2: libtmux Server connection works - Test 3: Server can list existing sessions Create TmuxSession dataclass and initial TmuxSessionManager: ```python from dataclasses import dataclass from datetime import datetime from typing import Optional, List import uuid import libtmux @dataclass class TmuxSession: """Represents a managed tmux session.""" session_id: str project_name: str tmux_session_name: str tmux_session: Optional[libtmux.Session] = None # Runtime reference @property def exists(self) -> bool: if self.tmux_session is None: return False try: # Check if session still exists in tmux self.tmux_session.session_id return True except libtmux.exc.LibTmuxException: return False class TmuxSessionManager: """Manages tmux session lifecycle using libtmux.""" SESSION_PREFIX = "gsd-" def __init__(self, server: Optional[libtmux.Server] = None): self.server = server or libtmux.Server() def _generate_session_name(self, project_name: str) -> str: """Generate tmux session name from project name.""" base_name = f"{self.SESSION_PREFIX}{project_name}" return base_name def _resolve_collision(self, base_name: str) -> str: """Add timestamp suffix if session name exists.""" existing = self.server.sessions.filter(session_name=base_name) if not existing: return base_name suffix = datetime.now().strftime("-%Y%m%d-%H%M%S") return f"{base_name}{suffix}" def list_sessions(self) -> List[libtmux.Session]: """List all gsd-managed sessions.""" return list(self.server.sessions.filter( session_name__startswith=self.SESSION_PREFIX )) ``` Add libtmux pytest fixtures to conftest.py for test isolation. pytest tests/test_tmux_session.py::test_server_connection -x pytest tests/test_tmux_session.py::test_list_sessions -x TmuxSession dataclass exists, TmuxSessionManager connects to libtmux, can list sessions Task 2: Implement session creation with state persistence src/gsd_openclaw/tmux_session.py, tests/test_tmux_session.py - Test 1: create_session creates tmux session with correct name - Test 2: create_session persists state to workspace - Test 3: create_session handles name collision with timestamp suffix - Test 4: create_session creates workspace directory if missing - Test 5: create_session returns TmuxSession with all metadata Implement create_session method: ```python from gsd_openclaw.state_manager import StateManager, SessionState from gsd_openclaw.workspace import WorkspaceManager class TmuxSessionManager: # ... existing code ... def create_session( self, project_name: str, workspace_manager: Optional[WorkspaceManager] = None, display_name: Optional[str] = None ) -> TmuxSession: """Create a new tmux session for a project.""" # Ensure workspace exists ws_manager = workspace_manager or WorkspaceManager(project_name) ws_manager.create() # Generate unique session name base_name = self._generate_session_name(project_name) tmux_name = self._resolve_collision(base_name) # Create tmux session (detached) tmux_session = self.server.new_session( session_name=tmux_name, attach=False, window_name='main' ) # Create session state session_id = str(uuid.uuid4())[:8] now = datetime.now().isoformat() state = SessionState( session_id=session_id, project_name=project_name, tmux_session_name=tmux_name, tmux_pane_id=tmux_session.active_pane.pane_id, created_at=now, last_active_at=now, display_name=display_name ) # Persist state state_manager = StateManager(project_name) state_manager.save(state) return TmuxSession( session_id=session_id, project_name=project_name, tmux_session_name=tmux_name, tmux_session=tmux_session ) ``` This integrates with StateManager and WorkspaceManager from previous plans. pytest tests/test_tmux_session.py::test_create_session -x pytest tests/test_tmux_session.py::test_session_naming_collision -x create_session creates tmux session, persists state, handles name collisions Task 3: Implement session destruction with cleanup src/gsd_openclaw/tmux_session.py, tests/test_tmux_session.py - Test 1: destroy_session kills tmux session - Test 2: destroy_session removes state file - Test 3: destroy_session handles already-destroyed session gracefully - Test 4: destroy_session with cleanup=True removes workspace Implement destroy_session method: ```python class TmuxSessionManager: # ... existing code ... def destroy_session( self, project_name: str, cleanup_workspace: bool = False ) -> bool: """Destroy a tmux session and optionally cleanup workspace.""" state_manager = StateManager(project_name) state = state_manager.load() if state is None: return False # No session to destroy # Kill tmux session try: tmux_session = self.server.sessions.get( session_name=state.tmux_session_name ) if tmux_session: tmux_session.kill() except libtmux.exc.LibTmuxException: pass # Session already gone # Remove state file if state_manager.state_path.exists(): state_manager.state_path.unlink() if state_manager.state_path.with_suffix('.json.bak').exists(): state_manager.state_path.with_suffix('.json.bak').unlink() # Optionally cleanup workspace if cleanup_workspace: workspace_manager = WorkspaceManager(project_name) workspace_manager.destroy() return True ``` The cleanup_workspace parameter allows keeping the workspace for later resumption if desired. pytest tests/test_tmux_session.py::test_destroy_session -x destroy_session kills tmux session, removes state, optionally cleans workspace Task 4: Implement session listing with project info src/gsd_openclaw/tmux_session.py, tests/test_tmux_session.py - Test 1: list_gsd_sessions returns all gsd-* sessions - Test 2: list_gsd_sessions includes project associations from state files - Test 3: list_gsd_sessions handles orphan sessions (no state file) Implement list_gsd_sessions method that combines tmux session info with state: ```python from dataclasses import dataclass @dataclass class SessionInfo: """Session info for display.""" tmux_name: str project_name: str session_id: str created_at: str last_active_at: str command_count: int display_name: Optional[str] is_orphan: bool = False # tmux session exists but no state class TmuxSessionManager: # ... existing code ... def list_gsd_sessions(self) -> List[SessionInfo]: """List all gsd sessions with project associations.""" sessions_info = [] # Get all gsd-* tmux sessions tmux_sessions = self.list_sessions() for tmux_session in tmux_sessions: tmux_name = tmux_session.name # Extract project name from tmux session name # Format: gsd-{project} or gsd-{project}-{timestamp} project_name = self._extract_project_name(tmux_name) # Try to load state state_manager = StateManager(project_name) state = state_manager.load() if state and state.tmux_session_name == tmux_name: # Matched session sessions_info.append(SessionInfo( tmux_name=tmux_name, project_name=project_name, session_id=state.session_id, created_at=state.created_at, last_active_at=state.last_active_at, command_count=state.command_count, display_name=state.display_name, is_orphan=False )) else: # Orphan session (no matching state) sessions_info.append(SessionInfo( tmux_name=tmux_name, project_name=project_name, session_id="?", created_at="?", last_active_at="?", command_count=0, display_name=None, is_orphan=True )) return sessions_info def _extract_project_name(self, tmux_name: str) -> str: """Extract project name from tmux session name.""" # Remove prefix name = tmux_name[len(self.SESSION_PREFIX):] # Remove timestamp suffix if present (format: -YYYYMMDD-HHMMSS) import re match = re.match(r'^(.+?)(-\d{8}-\d{6})?$', name) return match.group(1) if match else name ``` pytest tests/test_tmux_session.py::test_list_gsd_sessions -x list_gsd_sessions returns session info with project associations, handles orphans Run full test suite: ```bash pytest tests/test_tmux_session.py -v ``` Verify integration with state_manager and workspace: ```bash pytest tests/ -v -k "tmux or state or workspace" ``` - TmuxSession dataclass exists with metadata - create_session creates tmux session with libtmux - create_session persists state and handles name collisions - destroy_session kills tmux session and removes state - list_gsd_sessions returns session info with project associations - Concurrent sessions work without interference - All tests pass: `pytest tests/test_tmux_session.py -v` After completion, create `.planning/phases/01-core-infrastructure/01-03-SUMMARY.md`