14 KiB
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 | 04 | execute | 3 |
|
|
true |
|
|
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.
<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.mdTmuxSessionManager:
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:
@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:
class StateManager:
def save(self, state: SessionState) -> None: ...
def load(self) -> Optional[SessionState]: ...
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
)
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)
#!/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()
[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
Test CLI end-to-end:
pip install -e .
gsd-ocl create test-project
gsd-ocl list
gsd-ocl resume test-project
gsd-ocl destroy test-project
gsd-ocl cleanup
<success_criteria>
- 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</success_criteria>