--- phase: 01-core-infrastructure plan: 04 type: execute wave: 3 depends_on: - 01-03 files_modified: - src/gsd_openclaw/cli.py - src/gsd_openclaw/tmux_session.py - tests/test_cli.py - pyproject.toml autonomous: true requirements: - SESS-05 user_setup: [] must_haves: truths: - "User can resume an interrupted session with context restoration" - "User can invoke create/list/destroy/resume from command line" - "Orphan sessions are detected and can be cleaned up" - "State is validated before session resumption" artifacts: - path: "src/gsd_openclaw/cli.py" provides: "CLI entry point for session management" exports: ["main", "cli"] min_lines: 100 - path: "tests/test_cli.py" provides: "Test coverage for CLI commands" contains: "test_cli_create" key_links: - from: "src/gsd_openclaw/cli.py" to: "tmux_session.TmuxSessionManager" via: "CLI invokes session operations" pattern: "TmuxSessionManager" - from: "cli resume" to: "state_manager.load" via: "State validation before resumption" pattern: "load_state" --- Implement CLI entry point with session recovery and orphan cleanup. Purpose: Provides the user-facing interface for session management. Includes resume functionality that restores context from persisted state and cleanup for orphan sessions. Output: Working CLI with create/list/destroy/resume/cleanup commands. @/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 **TmuxSessionManager:** ```python class TmuxSessionManager: def create_session(self, project_name: str, ...) -> TmuxSession: ... def destroy_session(self, project_name: str, cleanup_workspace: bool) -> bool: ... def list_gsd_sessions(self) -> List[SessionInfo]: ... ``` **SessionInfo:** ```python @dataclass class SessionInfo: 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 ``` **StateManager:** ```python class StateManager: def save(self, state: SessionState) -> None: ... def load(self) -> Optional[SessionState]: ... ``` Task 1: Implement session resume with state validation src/gsd_openclaw/tmux_session.py, tests/test_tmux_session.py - Test 1: resume_session returns existing session if state valid and tmux exists - Test 2: resume_session recreates tmux session if state exists but tmux missing - Test 3: resume_session raises error if no state file exists - Test 4: resume_session validates schema version before resuming Add resume_session and get_session methods to TmuxSessionManager: ```python class TmuxSessionError(Exception): """Errors related to tmux session management.""" pass class TmuxSessionManager: # ... existing code ... def get_session(self, project_name: str) -> Optional[TmuxSession]: """Get existing session without creating new one.""" state_manager = StateManager(project_name) state = state_manager.load() if state is None: return None # Find tmux session try: tmux_session = self.server.sessions.get( session_name=state.tmux_session_name ) except libtmux.exc.LibTmuxException: tmux_session = None return TmuxSession( session_id=state.session_id, project_name=project_name, tmux_session_name=state.tmux_session_name, tmux_session=tmux_session ) def resume_session(self, project_name: str) -> TmuxSession: """Resume an existing session, recreating tmux if needed.""" state_manager = StateManager(project_name) state = state_manager.load() if state is None: raise TmuxSessionError(f"No session found for project: {project_name}") # Validate state if state.schema_version != "1.0": raise TmuxSessionError( f"Unsupported state version: {state.schema_version}" ) # Check if tmux session exists try: tmux_session = self.server.sessions.get( session_name=state.tmux_session_name ) except libtmux.exc.LibTmuxException: tmux_session = None # Recreate tmux session if missing if tmux_session is None: workspace_manager = WorkspaceManager(project_name) if not workspace_manager.exists(): raise TmuxSessionError( f"Workspace missing for project: {project_name}" ) # Create new tmux session with same name tmux_session = self.server.new_session( session_name=state.tmux_session_name, attach=False, window_name='main', start_directory=str(workspace_manager.get_workspace().root) ) # Update pane ID in state state.tmux_pane_id = tmux_session.active_pane.pane_id state.last_active_at = datetime.now().isoformat() state_manager.save(state) return TmuxSession( session_id=state.session_id, project_name=project_name, tmux_session_name=state.tmux_session_name, tmux_session=tmux_session ) ``` pytest tests/test_tmux_session.py::test_resume_session -x pytest tests/test_tmux_session.py::test_resume_recreates_tmux -x resume_session validates state, recreates tmux if needed, raises clear errors Task 2: Implement orphan session cleanup src/gsd_openclaw/tmux_session.py, tests/test_tmux_session.py - Test 1: cleanup_orphans kills tmux sessions without state files - Test 2: cleanup_orphans returns list of cleaned session names - Test 3: cleanup_orphans handles sessions with corrupted state files Add cleanup_orphans method: ```python from typing import List @dataclass class CleanupResult: """Result of orphan cleanup.""" cleaned_sessions: List[str] errors: List[str] class TmuxSessionManager: # ... existing code ... def cleanup_orphans(self) -> CleanupResult: """Remove tmux sessions without corresponding state files.""" cleaned = [] errors = [] # Get all gsd-* tmux sessions tmux_sessions = self.list_sessions() for tmux_session in tmux_sessions: tmux_name = tmux_session.name project_name = self._extract_project_name(tmux_name) # Check for state file state_manager = StateManager(project_name) state = state_manager.load() should_clean = False if state is None: # No state file - orphan should_clean = True elif state.tmux_session_name != tmux_name: # State exists but references different tmux session # This could be a collision case, treat as orphan should_clean = True if should_clean: try: tmux_session.kill() cleaned.append(tmux_name) except libtmux.exc.LibTmuxException as e: errors.append(f"Failed to kill {tmux_name}: {e}") return CleanupResult(cleaned_sessions=cleaned, errors=errors) ``` pytest tests/test_tmux_session.py::test_cleanup_orphans -x cleanup_orphans removes tmux sessions without state files, reports results Task 3: Implement CLI with argparse src/gsd_openclaw/cli.py, tests/test_cli.py - Test 1: `gsd-ocl create ` creates session - Test 2: `gsd-ocl list` shows sessions - Test 3: `gsd-ocl destroy ` removes session - Test 4: `gsd-ocl resume ` resumes session - Test 5: `gsd-ocl cleanup` removes orphans - Test 6: CLI exits with code 1 on error Create CLI using argparse following dev-workflow pattern: ```python #!/usr/bin/env python3 """CLI for gsd-openclaw session management.""" import argparse import sys from datetime import datetime from gsd_openclaw.tmux_session import ( TmuxSessionManager, TmuxSessionError, CleanupResult, ) def cmd_create(args): """Create a new session for a project.""" manager = TmuxSessionManager() try: session = manager.create_session( project_name=args.project, display_name=args.display_name ) print(f"Created session: {session.tmux_session_name}") print(f"Session ID: {session.session_id}") print(f"Project: {session.project_name}") except Exception as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) def cmd_list(args): """List all gsd sessions.""" manager = TmuxSessionManager() sessions = manager.list_gsd_sessions() if not sessions: print("No active sessions.") return print(f"{'Project':<20} {'Session ID':<10} {'Last Active':<20} {'Status'}") print("-" * 70) for s in sessions: status = "ORPHAN" if s.is_orphan else "active" display = s.display_name or s.project_name print(f"{display:<20} {s.session_id:<10} {s.last_active_at:<20} {status}") def cmd_destroy(args): """Destroy a session.""" manager = TmuxSessionManager() result = manager.destroy_session( project_name=args.project, cleanup_workspace=args.cleanup ) if result: print(f"Destroyed session for: {args.project}") else: print(f"No session found for: {args.project}") sys.exit(1) def cmd_resume(args): """Resume an existing session.""" manager = TmuxSessionManager() try: session = manager.resume_session(project_name=args.project) print(f"Resumed session: {session.tmux_session_name}") print(f"Session ID: {session.session_id}") print(f"Project: {session.project_name}") except TmuxSessionError as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) def cmd_cleanup(args): """Clean up orphan sessions.""" manager = TmuxSessionManager() result = manager.cleanup_orphans() if result.cleaned_sessions: print(f"Cleaned {len(result.cleaned_sessions)} orphan session(s):") for name in result.cleaned_sessions: print(f" - {name}") if result.errors: print(f"Errors ({len(result.errors)}):") for err in result.errors: print(f" - {err}") if not result.cleaned_sessions and not result.errors: print("No orphan sessions found.") def main(): parser = argparse.ArgumentParser( prog="gsd-ocl", description="GSD-OpenClaw session management" ) subparsers = parser.add_subparsers(dest="command", required=True) # create p_create = subparsers.add_parser("create", help="Create a new session") p_create.add_argument("project", help="Project name") p_create.add_argument("--display-name", "-d", help="Friendly display name") p_create.set_defaults(func=cmd_create) # list p_list = subparsers.add_parser("list", help="List all sessions") p_list.set_defaults(func=cmd_list) # destroy p_destroy = subparsers.add_parser("destroy", help="Destroy a session") p_destroy.add_argument("project", help="Project name") p_destroy.add_argument("--cleanup", "-c", action="store_true", help="Also remove workspace directory") p_destroy.set_defaults(func=cmd_destroy) # resume p_resume = subparsers.add_parser("resume", help="Resume a session") p_resume.add_argument("project", help="Project name") p_resume.set_defaults(func=cmd_resume) # cleanup p_cleanup = subparsers.add_parser("cleanup", help="Clean up orphan sessions") p_cleanup.set_defaults(func=cmd_cleanup) args = parser.parse_args() args.func(args) if __name__ == "__main__": main() ``` pytest tests/test_cli.py -x CLI provides create/list/destroy/resume/cleanup commands via argparse Task 4: Configure pyproject.toml with CLI entry point pyproject.toml Create or update pyproject.toml with package configuration: ```toml [build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] name = "gsd-openclaw" version = "0.1.0" description = "GSD to OpenClaw bridge skill" requires-python = ">=3.9" dependencies = [ "libtmux>=0.53.0", ] [project.optional-dependencies] dev = [ "pytest>=8.0", "pytest-cov>=4.0", ] [project.scripts] gsd-ocl = "gsd_openclaw.cli:main" [tool.setuptools.packages.find] where = ["src"] [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] python_classes = ["Test*"] python_functions = ["test_*"] addopts = "-v --tb=short" ``` This makes the CLI installable via `pip install -e .` and accessible as `gsd-ocl`. pip install -e . && gsd-ocl --help Package installable, gsd-ocl command available in PATH Run full test suite: ```bash pytest tests/ -v ``` Test CLI end-to-end: ```bash pip install -e . gsd-ocl create test-project gsd-ocl list gsd-ocl resume test-project gsd-ocl destroy test-project gsd-ocl cleanup ``` - resume_session validates state and recreates tmux if needed - cleanup_orphans removes orphan tmux sessions - CLI provides all session management commands - CLI exits with appropriate error codes - Package installable via pip - gsd-ocl command available in PATH - All tests pass: `pytest tests/ -v` After completion, create `.planning/phases/01-core-infrastructure/01-04-SUMMARY.md`