14 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | user_setup | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-core-infrastructure | 03 | execute | 2 |
|
|
true |
|
|
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.
<execution_context> @/home/starmen1265/.claude/get-shit-done/workflows/execute-plan.md @/home/starmen1265/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/01-core-infrastructure/01-CONTEXT.md @.planning/phases/01-core-infrastructure/01-RESEARCH.mdSessionState dataclass:
@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:
class StateManager:
def save(self, state: SessionState) -> None: ...
def load(self) -> Optional[SessionState]: ...
WorkspaceManager class:
class WorkspaceManager:
def create(self) -> Workspace: ...
def get_workspace(self) -> Workspace: ...
libtmux API:
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()
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: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: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: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
Verify integration with state_manager and workspace:
pytest tests/ -v -k "tmux or state or workspace"
<success_criteria>
- 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</success_criteria>