This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
---
|
||||
phase: 01-core-infrastructure
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/gsd_openclaw/__init__.py
|
||||
- src/gsd_openclaw/state_manager.py
|
||||
- tests/conftest.py
|
||||
- tests/test_state_manager.py
|
||||
autonomous: true
|
||||
requirements:
|
||||
- SESS-04
|
||||
user_setup: []
|
||||
must_haves:
|
||||
truths:
|
||||
- "State can be saved to JSON file atomically"
|
||||
- "State can be loaded from JSON file"
|
||||
- "Corrupted state falls back to backup automatically"
|
||||
- "State schema includes session metadata, tmux references, and activity stats"
|
||||
artifacts:
|
||||
- path: "src/gsd_openclaw/state_manager.py"
|
||||
provides: "State persistence with atomic writes"
|
||||
exports: ["SessionState", "StateManager", "save_state", "load_state"]
|
||||
min_lines: 80
|
||||
- path: "tests/test_state_manager.py"
|
||||
provides: "Test coverage for state operations"
|
||||
contains: "test_save_load_state"
|
||||
key_links:
|
||||
- from: "src/gsd_openclaw/state_manager.py"
|
||||
to: "~/gsd-workspaces/{project}/.gsd/state.json"
|
||||
via: "atomic write (tempfile + os.replace)"
|
||||
pattern: "tempfile\\.mkstemp.*os\\.replace"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Implement state persistence layer with atomic writes and backup rotation for crash-safe session checkpointing.
|
||||
|
||||
Purpose: Provides the foundation for all session state management. Other components depend on this for storing and recovering session metadata.
|
||||
|
||||
Output: Working state_manager.py with atomic save, load with validation, and automatic backup rotation.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/starmen1265/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/starmen1265/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<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.md
|
||||
@.planning/phases/01-core-infrastructure/01-VALIDATION.md
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
<!-- Key interfaces from RESEARCH.md code examples -->
|
||||
|
||||
**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
|
||||
```
|
||||
|
||||
**Atomic write pattern:**
|
||||
```python
|
||||
def save_state(state: dict, state_path: Path) -> None:
|
||||
fd, temp_path = tempfile.mkstemp(dir=state_path.parent, suffix='.tmp')
|
||||
try:
|
||||
with os.fdopen(fd, 'w', encoding='utf-8') as f:
|
||||
json.dump(state, f, indent=2, ensure_ascii=False)
|
||||
os.replace(temp_path, state_path)
|
||||
except Exception:
|
||||
if os.path.exists(temp_path):
|
||||
os.unlink(temp_path)
|
||||
raise
|
||||
```
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Create state schema with dataclasses</name>
|
||||
<files>src/gsd_openclaw/__init__.py, src/gsd_openclaw/state_manager.py</files>
|
||||
<behavior>
|
||||
- Test 1: SessionState can be created with all fields
|
||||
- Test 2: SessionState.to_dict() returns complete dict
|
||||
- Test 3: SessionState.from_dict() reconstructs from dict
|
||||
- Test 4: SessionState ignores unknown fields in from_dict()
|
||||
</behavior>
|
||||
<action>
|
||||
Create the SessionState dataclass with all fields from CONTEXT.md:
|
||||
- schema_version (default "1.0")
|
||||
- session_id, project_name, tmux_session_name, tmux_pane_id
|
||||
- created_at, last_active_at (ISO format strings)
|
||||
- command_count (int)
|
||||
- last_output_snapshot, display_name (Optional[str])
|
||||
|
||||
Implement to_dict() using dataclasses.asdict() and from_dict() with field filtering.
|
||||
|
||||
Also create the package __init__.py with version and exports.
|
||||
|
||||
Do NOT add validation logic yet - that comes in Task 3.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_state_manager.py::test_session_state_creation -x</automated>
|
||||
<automated>pytest tests/test_state_manager.py::test_session_state_serialization -x</automated>
|
||||
</verify>
|
||||
<done>SessionState dataclass exists with all fields, to_dict() and from_dict() methods work correctly</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Implement atomic state save with backup rotation</name>
|
||||
<files>src/gsd_openclaw/state_manager.py, tests/test_state_manager.py</files>
|
||||
<behavior>
|
||||
- Test 1: save_state creates file atomically (temp file + rename)
|
||||
- Test 2: save_state creates parent directories if missing
|
||||
- Test 3: save_state rotates backup (state.json -> state.json.bak)
|
||||
- Test 4: save_state cleans up temp file on failure
|
||||
- Test 5: Temp file is created in same directory as target (for atomic rename)
|
||||
</behavior>
|
||||
<action>
|
||||
Implement save_state(state: SessionState, state_path: Path) function:
|
||||
|
||||
1. Create parent directories with mkdir(parents=True, exist_ok=True)
|
||||
2. If state_path exists, rotate backup:
|
||||
- Delete old state.json.bak if exists
|
||||
- Rename state_path to state.json.bak
|
||||
3. Write to temp file using tempfile.mkstemp(dir=state_path.parent, suffix='.tmp')
|
||||
4. Use os.fdopen to write JSON with indent=2, ensure_ascii=False
|
||||
5. Atomic replace with os.replace(temp_path, state_path)
|
||||
6. On exception, clean up temp file if it exists
|
||||
|
||||
Use the pattern from RESEARCH.md - this is the atomic write pattern proven to prevent corruption.
|
||||
|
||||
Do NOT use file locking - os.replace is atomic on POSIX systems.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_state_manager.py::test_atomic_write -x</automated>
|
||||
<automated>pytest tests/test_state_manager.py::test_backup_rotation -x</automated>
|
||||
</verify>
|
||||
<done>State saves atomically, backup rotates correctly, temp files cleaned up on failure</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Implement state load with validation and fallback</name>
|
||||
<files>src/gsd_openclaw/state_manager.py, tests/test_state_manager.py, tests/conftest.py</files>
|
||||
<behavior>
|
||||
- Test 1: load_state returns None for missing file
|
||||
- Test 2: load_state returns SessionState for valid file
|
||||
- Test 3: load_state raises ValueError for unsupported schema version
|
||||
- Test 4: load_state raises ValueError for missing required fields
|
||||
- Test 5: load_state falls back to backup when main file is corrupted (JSONDecodeError)
|
||||
- Test 6: load_state falls back to backup when validation fails
|
||||
</behavior>
|
||||
<action>
|
||||
Implement load_state(state_path: Path) -> Optional[SessionState] function:
|
||||
|
||||
1. If state_path doesn't exist, try backup (state_path.with_suffix('.json.bak'))
|
||||
2. If neither exists, return None
|
||||
3. Read JSON with proper encoding (utf-8)
|
||||
4. Validate:
|
||||
- schema_version == "1.0" (raise ValueError if not)
|
||||
- Required fields present: session_id, project_name, tmux_session_name
|
||||
5. On JSONDecodeError or ValueError, try backup (if backup != state_path)
|
||||
6. Return SessionState.from_dict(data)
|
||||
|
||||
Also create tests/conftest.py with pytest fixtures:
|
||||
- tmp_state_dir fixture using pytest's tmp_path
|
||||
- sample_state fixture returning a valid SessionState instance
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_state_manager.py::test_save_load_state -x</automated>
|
||||
<automated>pytest tests/test_state_manager.py::test_state_validation -x</automated>
|
||||
<automated>pytest tests/test_state_manager.py::test_backup_fallback -x</automated>
|
||||
</verify>
|
||||
<done>State loads with validation, corrupted files fall back to backup, missing files return None</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: Create StateManager class with workspace path resolution</name>
|
||||
<files>src/gsd_openclaw/state_manager.py, tests/test_state_manager.py</files>
|
||||
<behavior>
|
||||
- Test 1: StateManager.get_state_path returns correct path for project
|
||||
- Test 2: StateManager.save persists state to correct location
|
||||
- Test 3: StateManager.load retrieves state from correct location
|
||||
</behavior>
|
||||
<action>
|
||||
Create StateManager class that encapsulates state operations:
|
||||
|
||||
```python
|
||||
class StateManager:
|
||||
WORKSPACES_ROOT = Path.home() / "gsd-workspaces"
|
||||
|
||||
def __init__(self, project_name: str):
|
||||
self.project_name = project_name
|
||||
self.workspace_path = self.WORKSPACES_ROOT / project_name
|
||||
self.state_path = self.workspace_path / ".gsd" / "state.json"
|
||||
|
||||
def save(self, state: SessionState) -> None:
|
||||
save_state(state, self.state_path)
|
||||
|
||||
def load(self) -> Optional[SessionState]:
|
||||
return load_state(self.state_path)
|
||||
|
||||
def get_state_path(self) -> Path:
|
||||
return self.state_path
|
||||
```
|
||||
|
||||
This provides a clean interface for other components to use state persistence without managing paths directly.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_state_manager.py::test_state_manager_class -x</automated>
|
||||
</verify>
|
||||
<done>StateManager class provides clean interface for state persistence with correct workspace paths</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
Run full test suite to verify all state operations:
|
||||
```bash
|
||||
pytest tests/test_state_manager.py -v
|
||||
```
|
||||
|
||||
All tests must pass. Coverage should include:
|
||||
- Atomic write pattern
|
||||
- Backup rotation
|
||||
- Validation logic
|
||||
- Fallback to backup
|
||||
- StateManager convenience methods
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- SessionState dataclass with all required fields exists
|
||||
- Atomic save_state function prevents corruption
|
||||
- load_state validates schema and required fields
|
||||
- Backup rotation works (state.json.bak)
|
||||
- Corrupted files fall back to backup automatically
|
||||
- StateManager class provides clean interface
|
||||
- All tests pass: `pytest tests/test_state_manager.py -v`
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-core-infrastructure/01-01-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
phase: 01-core-infrastructure
|
||||
plan: 01
|
||||
subsystem: state
|
||||
tags: [python, dataclasses, json, atomic-write, backup, persistence]
|
||||
|
||||
# Dependency graph
|
||||
requires: []
|
||||
provides:
|
||||
- SessionState dataclass for session metadata
|
||||
- Atomic save_state() function with backup rotation
|
||||
- load_state() function with validation and fallback
|
||||
- StateManager class for workspace-based state management
|
||||
affects: [02-tmux-session, 03-command-bridge, 04-gsd-integration]
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: [python dataclasses, json, tempfile, os.replace]
|
||||
patterns: [atomic write pattern, backup rotation, validation with fallback]
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- src/gsd_openclaw/state_manager.py
|
||||
- tests/test_state_manager.py
|
||||
modified:
|
||||
- src/gsd_openclaw/__init__.py
|
||||
- tests/conftest.py
|
||||
- pyproject.toml
|
||||
|
||||
key-decisions:
|
||||
- "Use tempfile.mkstemp + os.replace for atomic writes (POSIX guarantee)"
|
||||
- "Keep single backup (.bak) rather than backup history"
|
||||
- "Validate schema version and required fields on load"
|
||||
- "Fall back to backup on JSON or validation errors"
|
||||
|
||||
patterns-established:
|
||||
- "Atomic write pattern: temp file in target dir, os.replace for atomic rename"
|
||||
- "Backup rotation: rename existing state.json to state.json.bak before write"
|
||||
- "Validation pattern: check schema version first, then required fields"
|
||||
- "Fallback pattern: try main file, then backup, raise last error if both fail"
|
||||
|
||||
requirements-completed: [SESS-04]
|
||||
|
||||
# Metrics
|
||||
duration: 7min
|
||||
completed: 2026-03-10
|
||||
---
|
||||
|
||||
# Phase 1 Plan 1: State Persistence Layer Summary
|
||||
|
||||
**SessionState dataclass with atomic saves, backup rotation, and automatic fallback for crash-safe persistence**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 7 min
|
||||
- **Started:** 2026-03-10T16:32:46Z
|
||||
- **Completed:** 2026-03-10T16:39:55Z
|
||||
- **Tasks:** 4
|
||||
- **Files modified:** 5
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- SessionState dataclass with all required fields for session metadata
|
||||
- Atomic save_state() using tempfile + os.replace pattern
|
||||
- load_state() with schema validation and automatic backup fallback
|
||||
- StateManager class providing clean interface with workspace path resolution
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **Task 1: Create state schema with dataclasses** - `9ef5733` (feat)
|
||||
2. **Task 2: Implement atomic state save with backup rotation** - `8e0c973` (feat)
|
||||
3. **Task 3: Implement state load with validation and fallback** - `b221c5a` (feat)
|
||||
4. **Task 4: Create StateManager class with workspace path resolution** - `b2175b3` (feat)
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `src/gsd_openclaw/state_manager.py` - SessionState dataclass, save_state, load_state, StateManager
|
||||
- `src/gsd_openclaw/__init__.py` - Package exports
|
||||
- `tests/test_state_manager.py` - 25 comprehensive tests
|
||||
- `tests/conftest.py` - Added tmp_state_dir and sample_state fixtures
|
||||
- `pyproject.toml` - Python package configuration
|
||||
|
||||
## Decisions Made
|
||||
|
||||
- **Atomic write pattern**: Use tempfile.mkstemp in target directory, then os.replace for POSIX-atomic rename
|
||||
- **Backup strategy**: Single backup file (state.json.bak), rotated on each save
|
||||
- **Validation**: Schema version must be "1.0", required fields: session_id, project_name, tmux_session_name
|
||||
- **Fallback behavior**: On JSON or validation errors, automatically try backup file
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None - plan executed exactly as written.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
None - all tasks completed smoothly following TDD approach.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None - no external service configuration required.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- State persistence layer complete and tested
|
||||
- Ready for Phase 1 Plan 02 (tmux session management)
|
||||
- StateManager can be imported by other components for state access
|
||||
|
||||
---
|
||||
*Phase: 01-core-infrastructure*
|
||||
*Completed: 2026-03-10*
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- All source files exist and verified
|
||||
- All 4 task commits verified in git log
|
||||
- All 25 tests passing
|
||||
@@ -0,0 +1,256 @@
|
||||
---
|
||||
phase: 01-core-infrastructure
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/gsd_openclaw/workspace.py
|
||||
- tests/test_workspace.py
|
||||
autonomous: true
|
||||
requirements:
|
||||
- SESS-06
|
||||
user_setup: []
|
||||
must_haves:
|
||||
truths:
|
||||
- "Each project has its own isolated workspace directory"
|
||||
- "Workspace directory structure follows the defined layout"
|
||||
- "Workspace can be created on demand"
|
||||
- "Workspace can be destroyed (cleanup on session destroy)"
|
||||
artifacts:
|
||||
- path: "src/gsd_openclaw/workspace.py"
|
||||
provides: "Workspace directory management"
|
||||
exports: ["Workspace", "WorkspaceManager"]
|
||||
min_lines: 60
|
||||
- path: "tests/test_workspace.py"
|
||||
provides: "Test coverage for workspace operations"
|
||||
contains: "test_workspace_isolation"
|
||||
key_links:
|
||||
- from: "src/gsd_openclaw/workspace.py"
|
||||
to: "~/gsd-workspaces/"
|
||||
via: "Path.home() / gsd-workspaces"
|
||||
pattern: "Path\\.home.*gsd-workspaces"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Implement workspace directory management for project isolation.
|
||||
|
||||
Purpose: Each project gets its own isolated workspace with the defined directory structure. This ensures SESS-06 (isolated workspace per project).
|
||||
|
||||
Output: Working workspace.py with directory creation, structure validation, and cleanup.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/starmen1265/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/starmen1265/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<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.md
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
<!-- Workspace structure from CONTEXT.md -->
|
||||
|
||||
**Directory layout:**
|
||||
```
|
||||
~/gsd-workspaces/{project}/
|
||||
├── .gsd/
|
||||
│ ├── state.json # Session state
|
||||
│ ├── state.json.bak # State backup
|
||||
│ └── session-dump/ # Session dump
|
||||
├── code/ # Project code (optional)
|
||||
└── io-logs/
|
||||
└── {session-id}.log # I/O logs
|
||||
```
|
||||
|
||||
**Key paths:**
|
||||
- Root: `Path.home() / "gsd-workspaces" / project_name`
|
||||
- State: `workspace_root / ".gsd" / "state.json"`
|
||||
- I/O logs: `workspace_root / "io-logs"`
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Implement Workspace dataclass with path properties</name>
|
||||
<files>src/gsd_openclaw/workspace.py, tests/test_workspace.py</files>
|
||||
<behavior>
|
||||
- Test 1: Workspace creates correct paths for all directories
|
||||
- Test 2: Workspace paths are consistent (same root)
|
||||
- Test 3: Workspace handles project names with special characters safely
|
||||
</behavior>
|
||||
<action>
|
||||
Create Workspace dataclass that represents a project workspace:
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
@dataclass
|
||||
class Workspace:
|
||||
project_name: str
|
||||
root: Path = None # Computed property
|
||||
|
||||
def __post_init__(self):
|
||||
if self.root is None:
|
||||
self.root = Path.home() / "gsd-workspaces" / self.project_name
|
||||
|
||||
@property
|
||||
def gsd_dir(self) -> Path:
|
||||
return self.root / ".gsd"
|
||||
|
||||
@property
|
||||
def state_path(self) -> Path:
|
||||
return self.gsd_dir / "state.json"
|
||||
|
||||
@property
|
||||
def state_backup_path(self) -> Path:
|
||||
return self.gsd_dir / "state.json.bak"
|
||||
|
||||
@property
|
||||
def session_dump_dir(self) -> Path:
|
||||
return self.gsd_dir / "session-dump"
|
||||
|
||||
@property
|
||||
def code_dir(self) -> Path:
|
||||
return self.root / "code"
|
||||
|
||||
@property
|
||||
def io_logs_dir(self) -> Path:
|
||||
return self.root / "io-logs"
|
||||
|
||||
def io_log_path(self, session_id: str) -> Path:
|
||||
return self.io_logs_dir / f"{session_id}.log"
|
||||
```
|
||||
|
||||
For testing, allow injecting a custom root path (for tmp_path isolation).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_workspace.py::test_workspace_paths -x</automated>
|
||||
</verify>
|
||||
<done>Workspace dataclass exists with correct path properties for all directories</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Implement WorkspaceManager with create/destroy operations</name>
|
||||
<files>src/gsd_openclaw/workspace.py, tests/test_workspace.py</files>
|
||||
<behavior>
|
||||
- Test 1: create() creates all required directories
|
||||
- Test 2: create() is idempotent (calling twice doesn't fail)
|
||||
- Test 3: destroy() removes entire workspace directory
|
||||
- Test 4: exists() returns True after create, False after destroy
|
||||
- Test 5: Two projects have isolated workspaces (different roots)
|
||||
</behavior>
|
||||
<action>
|
||||
Create WorkspaceManager class:
|
||||
|
||||
```python
|
||||
class WorkspaceManager:
|
||||
WORKSPACES_ROOT = Path.home() / "gsd-workspaces"
|
||||
|
||||
def __init__(self, project_name: str, root: Optional[Path] = None):
|
||||
self.project_name = project_name
|
||||
self.root = root or self.WORKSPACES_ROOT
|
||||
self.workspace = Workspace(project_name, self.root / project_name)
|
||||
|
||||
def create(self) -> Workspace:
|
||||
"""Create workspace directory structure. Idempotent."""
|
||||
# Create all directories
|
||||
self.workspace.gsd_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.workspace.session_dump_dir.mkdir(exist_ok=True)
|
||||
self.workspace.code_dir.mkdir(exist_ok=True)
|
||||
self.workspace.io_logs_dir.mkdir(exist_ok=True)
|
||||
return self.workspace
|
||||
|
||||
def destroy(self) -> None:
|
||||
"""Remove entire workspace directory."""
|
||||
import shutil
|
||||
if self.workspace.root.exists():
|
||||
shutil.rmtree(self.workspace.root)
|
||||
|
||||
def exists(self) -> bool:
|
||||
"""Check if workspace exists."""
|
||||
return self.workspace.root.exists()
|
||||
|
||||
def get_workspace(self) -> Workspace:
|
||||
"""Get workspace object (does not create)."""
|
||||
return self.workspace
|
||||
```
|
||||
|
||||
The root parameter allows testing with tmp_path instead of home directory.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_workspace.py::test_workspace_create -x</automated>
|
||||
<automated>pytest tests/test_workspace.py::test_workspace_destroy -x</automated>
|
||||
<automated>pytest tests/test_workspace.py::test_workspace_isolation -x</automated>
|
||||
</verify>
|
||||
<done>WorkspaceManager creates/destroys workspace directories, multiple projects are isolated</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Create test fixtures for workspace testing</name>
|
||||
<files>tests/conftest.py</files>
|
||||
<action>
|
||||
Add workspace fixtures to conftest.py:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from gsd_openclaw.workspace import Workspace, WorkspaceManager
|
||||
|
||||
@pytest.fixture
|
||||
def tmp_workspaces_root(tmp_path: Path) -> Path:
|
||||
"""Temporary root for workspace testing."""
|
||||
return tmp_path / "gsd-workspaces"
|
||||
|
||||
@pytest.fixture
|
||||
def workspace_manager(tmp_workspaces_root: Path) -> WorkspaceManager:
|
||||
"""WorkspaceManager with temporary root for isolation."""
|
||||
manager = WorkspaceManager("test-project", tmp_workspaces_root)
|
||||
yield manager
|
||||
# Cleanup
|
||||
if manager.workspace.root.exists():
|
||||
import shutil
|
||||
shutil.rmtree(manager.workspace.root)
|
||||
```
|
||||
|
||||
This ensures tests don't pollute the real ~/gsd-workspaces directory.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_workspace.py -v</automated>
|
||||
</verify>
|
||||
<done>Test fixtures exist for isolated workspace testing</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
Run full test suite:
|
||||
```bash
|
||||
pytest tests/test_workspace.py -v
|
||||
```
|
||||
|
||||
Verify:
|
||||
- Directory structure created correctly
|
||||
- Idempotent create (no error on second call)
|
||||
- Destroy removes all files
|
||||
- Isolation between projects
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Workspace dataclass with all path properties exists
|
||||
- WorkspaceManager creates directory structure
|
||||
- WorkspaceManager destroys workspace cleanly
|
||||
- Multiple projects have isolated workspaces
|
||||
- All tests pass: `pytest tests/test_workspace.py -v`
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-core-infrastructure/01-02-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,131 @@
|
||||
---
|
||||
phase: 01-core-infrastructure
|
||||
plan: 02
|
||||
subsystem: infra
|
||||
tags: [workspace, directory-management, project-isolation, pytest]
|
||||
|
||||
# Dependency graph
|
||||
requires: []
|
||||
provides:
|
||||
- Workspace dataclass with path properties for isolated project directories
|
||||
- WorkspaceManager for workspace lifecycle (create/destroy/exists)
|
||||
- Test fixtures for isolated workspace testing
|
||||
affects: [session-management, gsd-integration]
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: [pytest, dataclasses]
|
||||
patterns: [fixture-based testing, temporary directory isolation]
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- src/gsd_openclaw/workspace.py
|
||||
- tests/conftest.py
|
||||
modified:
|
||||
- tests/test_workspace.py
|
||||
|
||||
key-decisions:
|
||||
- "Allow custom root path injection for testing isolation"
|
||||
- "Idempotent create() - safe to call multiple times"
|
||||
- "Use dataclass for Workspace to reduce boilerplate"
|
||||
|
||||
patterns-established:
|
||||
- "Pattern 1: tmp_path fixture for isolated directory testing"
|
||||
- "Pattern 2: yield fixtures with automatic cleanup"
|
||||
|
||||
requirements-completed: [SESS-06]
|
||||
|
||||
# Metrics
|
||||
duration: 4 min
|
||||
completed: 2026-03-10
|
||||
---
|
||||
|
||||
# Phase 1 Plan 2: Workspace Directory Management Summary
|
||||
|
||||
**Workspace dataclass and WorkspaceManager for project-isolated directory structure with TDD-tested create/destroy/exists operations.**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 4 min
|
||||
- **Started:** 2026-03-10T16:32:56Z
|
||||
- **Completed:** 2026-03-10T16:37:03Z
|
||||
- **Tasks:** 3
|
||||
- **Files modified:** 4
|
||||
|
||||
## Accomplishments
|
||||
- Workspace dataclass with all path properties (root, gsd_dir, state_path, etc.)
|
||||
- WorkspaceManager with idempotent create(), destroy(), exists() operations
|
||||
- Test fixtures for isolated workspace testing (tmp_workspaces_root, workspace_manager)
|
||||
- Project isolation verified - multiple projects have separate workspace directories
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **Task 1: Implement Workspace dataclass with path properties** - `2a0a643` (test - RED phase)
|
||||
2. **Task 1 & 2: Implement Workspace and WorkspaceManager** - `9acf258` (feat - GREEN phase)
|
||||
3. **Task 3: Create test fixtures for workspace testing** - `6e41bff` (test)
|
||||
|
||||
**Plan metadata:** (pending)
|
||||
|
||||
_Note: TDD tasks had multiple commits (test -> feat)_
|
||||
|
||||
## Files Created/Modified
|
||||
- `src/gsd_openclaw/workspace.py` - Workspace dataclass and WorkspaceManager (143 lines)
|
||||
- `tests/test_workspace.py` - Test cases for workspace operations (9 tests)
|
||||
- `tests/conftest.py` - Pytest fixtures for isolated testing
|
||||
- `src/gsd_openclaw/__init__.py` - Updated exports to include Workspace, WorkspaceManager
|
||||
|
||||
## Decisions Made
|
||||
- Allow custom root path injection for testing (avoids polluting ~/gsd-workspaces)
|
||||
- Idempotent create() using mkdir(exist_ok=True)
|
||||
- Use Python dataclass for Workspace to reduce boilerplate code
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 3 - Blocking] Created virtual environment for pytest**
|
||||
- **Found during:** Task 1 (test execution)
|
||||
- **Issue:** System Python is externally managed, cannot install pytest globally
|
||||
- **Fix:** Created .venv virtual environment and installed pytest there
|
||||
- **Files modified:** .venv/ (created)
|
||||
- **Verification:** Tests run successfully with .venv/bin/python -m pytest
|
||||
- **Committed in:** Not committed (venv is in .gitignore by convention)
|
||||
|
||||
**2. [Rule 3 - Blocking] Fixed __init__.py import conflict**
|
||||
- **Found during:** Task 1 (test execution)
|
||||
- **Issue:** __init__.py tried to import StateManager which raised NotImplementedError on import
|
||||
- **Fix:** Updated __init__.py to only import SessionState from state_manager, added Workspace exports
|
||||
- **Files modified:** src/gsd_openclaw/__init__.py
|
||||
- **Verification:** All 9 tests pass
|
||||
- **Committed in:** 9acf258 (Task 1 & 2 commit)
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 2 auto-fixed (2 blocking)
|
||||
**Impact on plan:** Both were necessary to complete the task. No scope creep.
|
||||
|
||||
## Issues Encountered
|
||||
None - all tasks completed as planned after resolving blocking issues.
|
||||
|
||||
## User Setup Required
|
||||
None - no external service configuration required.
|
||||
|
||||
## Next Phase Readiness
|
||||
- Workspace infrastructure ready for session management integration
|
||||
- Test fixtures established for future workspace-dependent tests
|
||||
- Directory structure follows CONTEXT.md specification
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- workspace.py: FOUND
|
||||
- test_workspace.py: FOUND
|
||||
- conftest.py: FOUND
|
||||
- Commit 2a0a643: FOUND
|
||||
- Commit 9acf258: FOUND
|
||||
- Commit 6e41bff: FOUND
|
||||
|
||||
---
|
||||
*Phase: 01-core-infrastructure*
|
||||
*Completed: 2026-03-10*
|
||||
@@ -0,0 +1,457 @@
|
||||
---
|
||||
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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/starmen1265/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/starmen1265/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<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.md
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
<!-- From Plan 01-01 (State Manager) -->
|
||||
|
||||
**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]: ...
|
||||
```
|
||||
|
||||
<!-- From Plan 01-02 (Workspace) -->
|
||||
|
||||
**WorkspaceManager class:**
|
||||
```python
|
||||
class WorkspaceManager:
|
||||
def create(self) -> Workspace: ...
|
||||
def get_workspace(self) -> Workspace: ...
|
||||
```
|
||||
|
||||
<!-- From RESEARCH.md - libtmux patterns -->
|
||||
|
||||
**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()
|
||||
```
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Implement TmuxSession dataclass and basic libtmux wrapper</name>
|
||||
<files>src/gsd_openclaw/tmux_session.py, tests/conftest.py, tests/test_tmux_session.py</files>
|
||||
<behavior>
|
||||
- Test 1: TmuxSession stores session metadata correctly
|
||||
- Test 2: libtmux Server connection works
|
||||
- Test 3: Server can list existing sessions
|
||||
</behavior>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_tmux_session.py::test_server_connection -x</automated>
|
||||
<automated>pytest tests/test_tmux_session.py::test_list_sessions -x</automated>
|
||||
</verify>
|
||||
<done>TmuxSession dataclass exists, TmuxSessionManager connects to libtmux, can list sessions</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Implement session creation with state persistence</name>
|
||||
<files>src/gsd_openclaw/tmux_session.py, tests/test_tmux_session.py</files>
|
||||
<behavior>
|
||||
- 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
|
||||
</behavior>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_tmux_session.py::test_create_session -x</automated>
|
||||
<automated>pytest tests/test_tmux_session.py::test_session_naming_collision -x</automated>
|
||||
</verify>
|
||||
<done>create_session creates tmux session, persists state, handles name collisions</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Implement session destruction with cleanup</name>
|
||||
<files>src/gsd_openclaw/tmux_session.py, tests/test_tmux_session.py</files>
|
||||
<behavior>
|
||||
- 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
|
||||
</behavior>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_tmux_session.py::test_destroy_session -x</automated>
|
||||
</verify>
|
||||
<done>destroy_session kills tmux session, removes state, optionally cleans workspace</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 4: Implement session listing with project info</name>
|
||||
<files>src/gsd_openclaw/tmux_session.py, tests/test_tmux_session.py</files>
|
||||
<behavior>
|
||||
- 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)
|
||||
</behavior>
|
||||
<action>
|
||||
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
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_tmux_session.py::test_list_gsd_sessions -x</automated>
|
||||
</verify>
|
||||
<done>list_gsd_sessions returns session info with project associations, handles orphans</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
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"
|
||||
```
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-core-infrastructure/01-03-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,141 @@
|
||||
---
|
||||
phase: 01-core-infrastructure
|
||||
plan: 03
|
||||
subsystem: tmux
|
||||
tags: [libtmux, session-management, lifecycle, state-persistence]
|
||||
|
||||
# Dependency graph
|
||||
requires:
|
||||
- phase: 01-01
|
||||
provides: StateManager, SessionState for persistence
|
||||
- phase: 01-02
|
||||
provides: WorkspaceManager, Workspace for project isolation
|
||||
provides:
|
||||
- TmuxSession dataclass for session metadata
|
||||
- TmuxSessionManager for create/list/destroy operations
|
||||
- SessionInfo for display with project associations
|
||||
- Session name collision handling with timestamp suffix
|
||||
affects: [command-execution, io-logging]
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: [libtmux>=0.55.0]
|
||||
patterns: [mock-based testing, state integration]
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- src/gsd_openclaw/tmux_session.py
|
||||
- tests/test_tmux_session.py
|
||||
modified:
|
||||
- src/gsd_openclaw/__init__.py
|
||||
- pyproject.toml
|
||||
|
||||
key-decisions:
|
||||
- "Mock-based testing for tmux integration (no real tmux server required)"
|
||||
- "Timestamp suffix format -YYYYMMDD-HHMMSS for collision resolution"
|
||||
- "Orphan session detection when tmux session exists without state file"
|
||||
|
||||
patterns-established:
|
||||
- "Pattern 1: TmuxSession dataclass with runtime reference and exists property"
|
||||
- "Pattern 2: TmuxSessionManager integrates with StateManager and WorkspaceManager"
|
||||
- "Pattern 3: Session naming with gsd-{project} prefix and collision handling"
|
||||
|
||||
requirements-completed: [SESS-01, SESS-02, SESS-03, SESS-07]
|
||||
|
||||
# Metrics
|
||||
duration: 6min
|
||||
completed: 2026-03-10
|
||||
---
|
||||
|
||||
# Phase 1 Plan 3: Tmux Session Lifecycle Summary
|
||||
|
||||
**Tmux session lifecycle management using libtmux with state persistence and workspace integration**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 6 min
|
||||
- **Started:** 2026-03-10T16:42:50Z
|
||||
- **Completed:** 2026-03-10T16:49:12Z
|
||||
- **Tasks:** 4
|
||||
- **Files modified:** 4
|
||||
|
||||
## Accomplishments
|
||||
- TmuxSession dataclass with metadata and exists property for session validation
|
||||
- TmuxSessionManager for create/list/destroy operations with libtmux integration
|
||||
- Session name collision handling with timestamp suffix format
|
||||
- Session listing with project associations and orphan detection
|
||||
- Integration with StateManager for persistence and WorkspaceManager for isolation
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **Task 1-4: Implement tmux session lifecycle management** - `082ed4b` (feat)
|
||||
- All tasks implemented together as they form a cohesive unit
|
||||
- TmuxSession dataclass, TmuxSessionManager class, SessionInfo dataclass
|
||||
- 22 tests covering all functionality
|
||||
|
||||
**Plan metadata:** pending (will be added with final commit)
|
||||
|
||||
_Note: All tasks were implemented together as the TmuxSessionManager class provides all required methods_
|
||||
|
||||
## Files Created/Modified
|
||||
- `src/gsd_openclaw/tmux_session.py` - Tmux session lifecycle management (317 lines)
|
||||
- `tests/test_tmux_session.py` - Test coverage for tmux operations (460 lines, 22 tests)
|
||||
- `src/gsd_openclaw/__init__.py` - Added exports for TmuxSession, TmuxSessionManager, SessionInfo
|
||||
- `pyproject.toml` - Added libtmux>=0.55.0 dependency
|
||||
|
||||
## Decisions Made
|
||||
- Mock-based testing approach chosen to avoid dependency on real tmux server during tests
|
||||
- Timestamp suffix format -YYYYMMDD-HHMMSS provides both uniqueness and human readability
|
||||
- Orphan sessions (tmux session without state file) are detected and marked in list_gsd_sessions
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 3 - Blocking] Added libtmux dependency**
|
||||
- **Found during:** Task 1 (test setup)
|
||||
- **Issue:** libtmux package was not installed, imports would fail
|
||||
- **Fix:** Added libtmux>=0.55.0 to pyproject.toml and ran pip install
|
||||
- **Files modified:** pyproject.toml
|
||||
- **Verification:** All imports succeed, tests pass
|
||||
- **Committed in:** 082ed4b (Task 1-4 commit)
|
||||
|
||||
**2. [Rule 3 - Blocking] Fixed test mocks for tmux server**
|
||||
- **Found during:** Task 1 (test execution)
|
||||
- **Issue:** Tests requiring real tmux server failed with TmuxCommandNotFound
|
||||
- **Fix:** Refactored tests to use mock objects instead of real tmux server
|
||||
- **Files modified:** tests/test_tmux_session.py
|
||||
- **Verification:** All 22 tests pass
|
||||
- **Committed in:** 082ed4b (Task 1-4 commit)
|
||||
|
||||
**3. [Rule 1 - Bug] Fixed PropertyMock usage in test**
|
||||
- **Found during:** Task 1 (test execution)
|
||||
- **Issue:** PropertyMock assignment was incorrect, test failed
|
||||
- **Fix:** Changed `mock_tmux.session_id = PropertyMock(...)` to `type(mock_tmux).session_id = PropertyMock(...)`
|
||||
- **Files modified:** tests/test_tmux_session.py
|
||||
- **Verification:** All tests pass
|
||||
- **Committed in:** 082ed4b (Task 1-4 commit)
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 3 auto-fixed (1 blocking, 1 blocking, 1 bug)
|
||||
**Impact on plan:** All auto-fixes necessary for correct testing without real tmux server. No scope creep.
|
||||
|
||||
## Issues Encountered
|
||||
None - all tasks completed successfully with mock-based testing approach.
|
||||
|
||||
## User Setup Required
|
||||
None - no external service configuration required. libtmux is a Python package installed automatically.
|
||||
|
||||
## Next Phase Readiness
|
||||
- Tmux session management complete, ready for command execution implementation
|
||||
- State persistence integration verified
|
||||
- Workspace isolation integration verified
|
||||
- All 56 tests passing across all modules
|
||||
|
||||
## Self-Check: PASSED
|
||||
- src/gsd_openclaw/tmux_session.py: FOUND
|
||||
- tests/test_tmux_session.py: FOUND
|
||||
- Commit 082ed4b: FOUND
|
||||
@@ -0,0 +1,500 @@
|
||||
---
|
||||
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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/starmen1265/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/starmen1265/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<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.md
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
<!-- From Plan 01-03 (Tmux Session) -->
|
||||
|
||||
**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
|
||||
```
|
||||
|
||||
<!-- From Plan 01-01 (State Manager) -->
|
||||
|
||||
**StateManager:**
|
||||
```python
|
||||
class StateManager:
|
||||
def save(self, state: SessionState) -> None: ...
|
||||
def load(self) -> Optional[SessionState]: ...
|
||||
```
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Implement session resume with state validation</name>
|
||||
<files>src/gsd_openclaw/tmux_session.py, tests/test_tmux_session.py</files>
|
||||
<behavior>
|
||||
- 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
|
||||
</behavior>
|
||||
<action>
|
||||
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
|
||||
)
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_tmux_session.py::test_resume_session -x</automated>
|
||||
<automated>pytest tests/test_tmux_session.py::test_resume_recreates_tmux -x</automated>
|
||||
</verify>
|
||||
<done>resume_session validates state, recreates tmux if needed, raises clear errors</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Implement orphan session cleanup</name>
|
||||
<files>src/gsd_openclaw/tmux_session.py, tests/test_tmux_session.py</files>
|
||||
<behavior>
|
||||
- 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
|
||||
</behavior>
|
||||
<action>
|
||||
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)
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_tmux_session.py::test_cleanup_orphans -x</automated>
|
||||
</verify>
|
||||
<done>cleanup_orphans removes tmux sessions without state files, reports results</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Implement CLI with argparse</name>
|
||||
<files>src/gsd_openclaw/cli.py, tests/test_cli.py</files>
|
||||
<behavior>
|
||||
- Test 1: `gsd-ocl create <project>` creates session
|
||||
- Test 2: `gsd-ocl list` shows sessions
|
||||
- Test 3: `gsd-ocl destroy <project>` removes session
|
||||
- Test 4: `gsd-ocl resume <project>` resumes session
|
||||
- Test 5: `gsd-ocl cleanup` removes orphans
|
||||
- Test 6: CLI exits with code 1 on error
|
||||
</behavior>
|
||||
<action>
|
||||
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()
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_cli.py -x</automated>
|
||||
</verify>
|
||||
<done>CLI provides create/list/destroy/resume/cleanup commands via argparse</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: Configure pyproject.toml with CLI entry point</name>
|
||||
<files>pyproject.toml</files>
|
||||
<action>
|
||||
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`.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pip install -e . && gsd-ocl --help</automated>
|
||||
</verify>
|
||||
<done>Package installable, gsd-ocl command available in PATH</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
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
|
||||
```
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-core-infrastructure/01-04-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,110 @@
|
||||
---
|
||||
phase: 01-core-infrastructure
|
||||
plan: 04
|
||||
subsystem: cli
|
||||
tags: [argparse, session-management, resume, cleanup, libtmux]
|
||||
|
||||
# Dependency graph
|
||||
requires:
|
||||
- phase: 01-03
|
||||
provides: TmuxSessionManager with create/list/destroy
|
||||
- phase: 01-01
|
||||
provides: StateManager for session state persistence
|
||||
provides:
|
||||
- CLI entry point for session management (gsd-ocl)
|
||||
- Session resume with state validation
|
||||
- Orphan session cleanup functionality
|
||||
affects: [02-claude-integration, 03-communication-bridge]
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: [argparse (stdlib)]
|
||||
patterns: [CLI subcommands pattern, mock-based testing]
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- src/gsd_openclaw/cli.py
|
||||
- tests/test_cli.py
|
||||
modified:
|
||||
- src/gsd_openclaw/tmux_session.py
|
||||
- tests/test_tmux_session.py
|
||||
- pyproject.toml
|
||||
|
||||
key-decisions:
|
||||
- "Argparse for CLI - standard library, no dependencies"
|
||||
- "Exit code 1 for all errors - simple error handling"
|
||||
- "TmuxSessionError exception class for session-specific errors"
|
||||
|
||||
patterns-established:
|
||||
- "CLI subcommands: each command is a separate function with args parameter"
|
||||
- "Mock-based testing for tmux operations - no real tmux server needed"
|
||||
|
||||
requirements-completed: [SESS-05]
|
||||
|
||||
# Metrics
|
||||
duration: 8min
|
||||
completed: 2026-03-10
|
||||
---
|
||||
|
||||
# Phase 1 Plan 04: CLI Entry Point Summary
|
||||
|
||||
**CLI with create/list/destroy/resume/cleanup commands using argparse, plus session resume with state validation and orphan cleanup**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 8 min
|
||||
- **Started:** 2026-03-10T16:52:20Z
|
||||
- **Completed:** 2026-03-10T17:00:20Z
|
||||
- **Tasks:** 4
|
||||
- **Files modified:** 5
|
||||
|
||||
## Accomplishments
|
||||
- Session resume functionality with state validation and schema version checking
|
||||
- Automatic tmux session recreation when state exists but tmux session is missing
|
||||
- Orphan session cleanup to remove tmux sessions without corresponding state files
|
||||
- Full CLI with argparse providing all session management commands
|
||||
- Package installable via pip with gsd-ocl entry point
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **Task 1: Implement session resume with state validation** - `f877b3c` (feat)
|
||||
2. **Task 2: Implement orphan session cleanup** - `8598b8e` (feat)
|
||||
3. **Task 3: Implement CLI with argparse** - `4d14600` (feat)
|
||||
4. **Task 4: Configure pyproject.toml with CLI entry point** - `8d0282a` (feat)
|
||||
|
||||
_Note: All tasks followed TDD pattern (RED-GREEN-REFACTOR)_
|
||||
|
||||
## Files Created/Modified
|
||||
- `src/gsd_openclaw/cli.py` - CLI entry point with argparse subcommands
|
||||
- `src/gsd_openclaw/tmux_session.py` - Added resume_session, get_session, cleanup_orphans, TmuxSessionError, CleanupResult
|
||||
- `tests/test_cli.py` - Comprehensive CLI tests (16 tests)
|
||||
- `tests/test_tmux_session.py` - Added resume and cleanup tests (11 new tests)
|
||||
- `pyproject.toml` - Added CLI entry point and package configuration
|
||||
|
||||
## Decisions Made
|
||||
- Argparse chosen for CLI - standard library, no external dependencies needed
|
||||
- TmuxSessionError exception class for clear error handling in session operations
|
||||
- CleanupResult dataclass to return both cleaned sessions and errors
|
||||
- Exit code 1 for all CLI errors - simple and consistent error handling
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None - plan executed exactly as written.
|
||||
|
||||
## Issues Encountered
|
||||
None - all tasks completed smoothly following TDD pattern.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None - no external service configuration required.
|
||||
|
||||
## Next Phase Readiness
|
||||
- Core infrastructure complete with workspace, state, tmux session, and CLI layers
|
||||
- Ready for Phase 2: Claude Integration
|
||||
- All 83 tests passing
|
||||
|
||||
---
|
||||
*Phase: 01-core-infrastructure*
|
||||
*Completed: 2026-03-10*
|
||||
@@ -0,0 +1,109 @@
|
||||
# Phase 1: Core Infrastructure - Context
|
||||
|
||||
**Gathered:** 2026-03-11
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
创建、列表、销毁、恢复 tmux 会话,带持久化状态,支持多用户隔离的会话管理基础设施。这是整个 gsd-openclaw 技能的底层核心,后续所有命令桥接和 GSD 集成都依赖于此。
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Session Naming
|
||||
- **命名格式**: 仅项目名(单用户场景,如 `gsd-project-alpha`)
|
||||
- **同名冲突**: 自动添加时间戳后缀(如 `-20260311-001`)
|
||||
- **显示名**: 支持用户自定义友好名称,存储映射:显示名 → tmux 名
|
||||
- **会话限制**: 每用户可配置最大会话数(默认值由 planner 决定)
|
||||
|
||||
### State Storage
|
||||
- **位置**: 每项目独立目录 `~/gsd-workspaces/{project}/.gsd/`
|
||||
- **检查点频率**: 每 30 秒自动保存
|
||||
- **状态内容**:
|
||||
- 会话元数据(ID、项目名、创建时间)
|
||||
- tmux 引用(会话名、窗格 ID)
|
||||
- 活动统计(最后活跃时间、命令计数)
|
||||
- 最后输出快照
|
||||
- **原子写入**: 是(先写临时文件再 rename)
|
||||
- **备份策略**: 保留 1 个备份(`.gsd/state.json.bak`)
|
||||
|
||||
### Workspace Structure
|
||||
- **根目录**: `~/gsd-workspaces/{project}/`
|
||||
- **目录布局**:
|
||||
```
|
||||
~/gsd-workspaces/{project}/
|
||||
├── .gsd/
|
||||
│ ├── state.json # 会话状态
|
||||
│ ├── state.json.bak # 状态备份
|
||||
│ └── session-dump/ # 会话转储
|
||||
├── code/ # 项目代码(可选)
|
||||
└── io-logs/
|
||||
└── {session-id}.log # I/O 日志
|
||||
```
|
||||
- **I/O 日志保留**: 7 天
|
||||
- **会话结束处理**: 销毁会话时删除工作空间
|
||||
|
||||
### Recovery Strategy
|
||||
- **崩溃恢复**: 启动时自动检测状态文件,提示用户是否恢复
|
||||
- **孤儿会话处理**: 自动清理没有状态文件的 tmux 会话
|
||||
- **状态验证**: 恢复前验证状态文件完整性和版本兼容性
|
||||
- **备份使用**: 主状态文件损坏时自动尝试备份
|
||||
|
||||
### Claude's Discretion
|
||||
- 检查点的精确触发时机(命令后 vs 定时器)
|
||||
- 状态文件 schema 版本号设计
|
||||
- 日志文件命名格式
|
||||
- 错误消息的具体措辞
|
||||
|
||||
</decisions>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
- 工作空间结构参考 dev-workflow 的 `.planning/` 模式
|
||||
- 状态文件使用 JSON 格式,与 dev-workflow 的 state.json 一致
|
||||
- tmux 会话名前缀统一使用 `gsd-` 避免与其他 tmux 用途冲突
|
||||
|
||||
</specifics>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
- **dev-workflow/state_manager.py**: JSON 状态管理模式可直接复用
|
||||
- `load_state()`, `save_state()`, `init_state()` 函数签名
|
||||
- 原子写入模式(temp file + rename)
|
||||
- ISO 时间戳格式
|
||||
- **dev-workflow/orchestrator.py**: 命令行参数解析模式
|
||||
- argparse 结构
|
||||
- 阶段管理逻辑
|
||||
|
||||
### Established Patterns
|
||||
- **状态路径查找**: 从当前目录向上查找 `.planning/` 定位项目根
|
||||
- **目录创建**: `mkdir(parents=True, exist_ok=True)` 模式
|
||||
- **错误处理**: 中文错误消息 + sys.exit(1)
|
||||
|
||||
### Integration Points
|
||||
- 本 phase 产出的 `state_manager.py` 将被 Phase 3 的 `orchestrator.py` 调用
|
||||
- `tmux_session_manager.py` 将被 Phase 3 的命令桥接调用
|
||||
- 工作空间目录结构将被 Phase 4 的 GSD 集成使用
|
||||
|
||||
</code_context>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
- 多用户前缀(当前为单用户场景,如需多用户隔离可后续添加)
|
||||
- 会话池化和 LRU 淘汰(v2 特性)
|
||||
- 会话分支和克隆(v2 特性)
|
||||
- 实时状态同步到 OpenClaw 控制面板(Phase 5)
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 01-core-infrastructure*
|
||||
*Context gathered: 2026-03-11*
|
||||
@@ -0,0 +1,460 @@
|
||||
# Phase 1: Core Infrastructure - Research
|
||||
|
||||
**Researched:** 2026-03-11
|
||||
**Domain:** tmux session management, state persistence, Python CLI
|
||||
**Confidence:** HIGH
|
||||
|
||||
## Summary
|
||||
|
||||
This phase implements the foundational infrastructure for gsd-openclaw: tmux session lifecycle management with persistent state checkpointing. The core technical stack centers on **libtmux** (v0.53.x) as the Python wrapper for tmux control, combined with JSON state files using atomic write patterns for crash-safe persistence.
|
||||
|
||||
**Primary recommendation:** Use libtmux for all tmux operations - it provides a clean Pythonic API with Server/Session/Window/Pane objects, built-in pytest fixtures for testing, and is battle-tested via tmuxp. Do not shell out to `tmux` commands directly.
|
||||
|
||||
<user_constraints>
|
||||
## User Constraints (from CONTEXT.md)
|
||||
|
||||
### Locked Decisions
|
||||
|
||||
**Session Naming:**
|
||||
- Naming format: Project name only (single-user scenario, e.g., `gsd-project-alpha`)
|
||||
- Name collision: Auto-add timestamp suffix (e.g., `-20260311-001`)
|
||||
- Display name: Support user-defined friendly names with mapping: display name -> tmux name
|
||||
- Session limit: Per-user configurable max sessions (default value by planner)
|
||||
|
||||
**State Storage:**
|
||||
- Location: Per-project independent directory `~/gsd-workspaces/{project}/.gsd/`
|
||||
- Checkpoint frequency: Auto-save every 30 seconds
|
||||
- State content:
|
||||
- Session metadata (ID, project name, creation time)
|
||||
- tmux reference (session name, pane ID)
|
||||
- Activity statistics (last active time, command count)
|
||||
- Last output snapshot
|
||||
- Atomic write: Yes (write to temp file then rename)
|
||||
- Backup strategy: Keep 1 backup (`.gsd/state.json.bak`)
|
||||
|
||||
**Workspace Structure:**
|
||||
- Root directory: `~/gsd-workspaces/{project}/`
|
||||
- Directory layout:
|
||||
```
|
||||
~/gsd-workspaces/{project}/
|
||||
├── .gsd/
|
||||
│ ├── state.json # Session state
|
||||
│ ├── state.json.bak # State backup
|
||||
│ └── session-dump/ # Session dump
|
||||
├── code/ # Project code (optional)
|
||||
└── io-logs/
|
||||
└── {session-id}.log # I/O logs
|
||||
```
|
||||
- I/O log retention: 7 days
|
||||
- Session end handling: Delete workspace when destroying session
|
||||
|
||||
**Recovery Strategy:**
|
||||
- Crash recovery: Auto-detect state file on startup, prompt user to restore
|
||||
- Orphan session handling: Auto-clean tmux sessions without state files
|
||||
- State validation: Validate state file integrity and version compatibility before restore
|
||||
- Backup usage: Auto-try backup if main state file is corrupted
|
||||
|
||||
### Claude's Discretion
|
||||
|
||||
- Exact checkpoint trigger timing (post-command vs timer)
|
||||
- State file schema version number design
|
||||
- Log file naming format
|
||||
- Specific error message wording
|
||||
|
||||
### Deferred Ideas (OUT OF SCOPE)
|
||||
|
||||
- Multi-user prefix (current single-user scenario; can add later for multi-user isolation)
|
||||
- Session pooling and LRU eviction (v2 feature)
|
||||
- Session branching and cloning (v2 feature)
|
||||
- Real-time state sync to OpenClaw control panel (Phase 5)
|
||||
|
||||
</user_constraints>
|
||||
|
||||
<phase_requirements>
|
||||
## Phase Requirements
|
||||
|
||||
| ID | Description | Research Support |
|
||||
|----|-------------|-----------------|
|
||||
| SESS-01 | User can create tmux session for a project workspace | libtmux `Server.new_session()` API |
|
||||
| SESS-02 | User can list active tmux sessions with project associations | libtmux `Server.sessions` QueryList with filter/get |
|
||||
| SESS-03 | User can destroy a tmux session when project is complete | libtmux `Session.kill()` method |
|
||||
| SESS-04 | Session state persists across server restarts via JSON checkpointing | Atomic write pattern with tempfile + os.replace |
|
||||
| SESS-05 | User can resume interrupted sessions with full context restoration | State validation + libtmux session reattachment |
|
||||
| SESS-06 | Each project has isolated workspace (separate tmux session + directory) | Workspace directory structure pattern |
|
||||
| SESS-07 | Multiple users can run projects concurrently without interference | Session naming with `gsd-` prefix (single-user for v1) |
|
||||
|
||||
</phase_requirements>
|
||||
|
||||
## Standard Stack
|
||||
|
||||
### Core
|
||||
| Library | Version | Purpose | Why Standard |
|
||||
|---------|---------|---------|--------------|
|
||||
| libtmux | 0.53.x | Python API for tmux control | Official Python wrapper, battle-tested via tmuxp, provides Server/Session/Window/Pane objects |
|
||||
| pathlib | stdlib | Path handling | Cross-platform, modern Python path API |
|
||||
| json | stdlib | State serialization | Human-readable, matches GSD patterns |
|
||||
| dataclasses | stdlib | State schema definition | Type-safe state models with minimal boilerplate |
|
||||
|
||||
### Supporting
|
||||
| Library | Version | Purpose | When to Use |
|
||||
|---------|---------|---------|-------------|
|
||||
| pytest | 8.x | Testing framework | All unit/integration tests |
|
||||
| pytest-asyncio | 0.23+ | Async test support | If checkpoint timer uses async |
|
||||
| typing | stdlib | Type hints | All function signatures |
|
||||
|
||||
### Alternatives Considered
|
||||
| Instead of | Could Use | Tradeoff |
|
||||
|------------|-----------|----------|
|
||||
| libtmux | subprocess + tmux CLI | libtmux provides typed objects, error handling, pytest fixtures; subprocess requires parsing output |
|
||||
| JSON | YAML/TOML | JSON is faster to parse, stdlib-only, matches GSD state.json pattern |
|
||||
| dataclasses | Pydantic | dataclasses is stdlib, sufficient for this use case; Pydantic adds validation overhead |
|
||||
|
||||
**Installation:**
|
||||
```bash
|
||||
pip install libtmux pytest pytest-asyncio
|
||||
```
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Recommended Project Structure
|
||||
```
|
||||
gsd-openclaw/
|
||||
├── src/
|
||||
│ └── gsd_openclaw/
|
||||
│ ├── __init__.py
|
||||
│ ├── state_manager.py # State persistence (load/save/validate)
|
||||
│ ├── tmux_session.py # tmux session lifecycle wrapper
|
||||
│ ├── workspace.py # Workspace directory management
|
||||
│ └── cli.py # CLI entry point (create/list/destroy/resume)
|
||||
├── tests/
|
||||
│ ├── conftest.py # pytest fixtures (libtmux server)
|
||||
│ ├── test_state_manager.py # State persistence tests
|
||||
│ ├── test_tmux_session.py # Session lifecycle tests
|
||||
│ └── test_workspace.py # Workspace management tests
|
||||
└── pyproject.toml
|
||||
```
|
||||
|
||||
### Pattern 1: Atomic State Write
|
||||
**What:** Write state to temp file, then atomically rename to target
|
||||
**When to use:** All state.json writes to prevent corruption on crash
|
||||
**Example:**
|
||||
```python
|
||||
# Source: https://blog.gocept.com/2013/07/15/reliable-file-updates-with-python/
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
def save_state(state: dict, state_path: Path) -> None:
|
||||
"""Atomically write state to JSON file."""
|
||||
state_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write to temp file in same directory (same filesystem for atomic rename)
|
||||
fd, temp_path = tempfile.mkstemp(dir=state_path.parent, suffix='.tmp')
|
||||
try:
|
||||
with os.fdopen(fd, 'w', encoding='utf-8') as f:
|
||||
json.dump(state, f, indent=2, ensure_ascii=False)
|
||||
# Atomic replace (os.replace is atomic on POSIX)
|
||||
os.replace(temp_path, state_path)
|
||||
except Exception:
|
||||
# Clean up temp file on failure
|
||||
if os.path.exists(temp_path):
|
||||
os.unlink(temp_path)
|
||||
raise
|
||||
```
|
||||
|
||||
### Pattern 2: libtmux Session Management
|
||||
**What:** Use libtmux Server/Session objects for all tmux operations
|
||||
**When to use:** All tmux session create/list/destroy/attach operations
|
||||
**Example:**
|
||||
```python
|
||||
# Source: https://github.com/tmux-python/libtmux
|
||||
import libtmux
|
||||
|
||||
server = libtmux.Server()
|
||||
|
||||
# Create new session
|
||||
session = server.new_session(
|
||||
session_name='gsd-my-project',
|
||||
attach=False, # Don't attach (detached mode)
|
||||
window_name='main'
|
||||
)
|
||||
|
||||
# List sessions
|
||||
for s in server.sessions:
|
||||
print(f"{s.name}: {s.session_id}")
|
||||
|
||||
# Find specific session
|
||||
session = server.sessions.get(session_name='gsd-my-project')
|
||||
|
||||
# Kill session
|
||||
session.kill()
|
||||
```
|
||||
|
||||
### Pattern 3: State Schema with dataclasses
|
||||
**What:** Define typed state schema using dataclasses
|
||||
**When to use:** All state.json structure definition
|
||||
**Example:**
|
||||
```python
|
||||
from dataclasses import dataclass, asdict
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
@dataclass
|
||||
class SessionState:
|
||||
"""State schema for a gsd-openclaw session."""
|
||||
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
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return asdict(self)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> 'SessionState':
|
||||
return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
|
||||
```
|
||||
|
||||
### Anti-Patterns to Avoid
|
||||
- **Shelling out to tmux CLI:** Use libtmux API instead of `subprocess.run(['tmux', ...])`
|
||||
- **Non-atomic writes:** Never write directly to state.json; always use temp file + rename
|
||||
- **Hardcoded paths:** Use `~/gsd-workspaces/` via `Path.home() / "gsd-workspaces"`
|
||||
- **Session name collisions:** Always check for existing sessions before creating new ones
|
||||
|
||||
## Don't Hand-Roll
|
||||
|
||||
| Problem | Don't Build | Use Instead | Why |
|
||||
|---------|-------------|-------------|-----|
|
||||
| tmux control | subprocess + tmux commands | libtmux | Error handling, typed objects, pytest fixtures built-in |
|
||||
| Session listing | Parse `tmux list-sessions` output | `server.sessions` QueryList | Filter/get methods, type safety |
|
||||
| Atomic writes | Custom file locking | tempfile + os.replace | OS-level atomicity, proven pattern |
|
||||
| Test tmux server | Manual setup/teardown | libtmux pytest fixtures | Auto cleanup, isolated servers |
|
||||
|
||||
**Key insight:** libtmux's pytest plugin provides `server`, `session`, and `TestServer` fixtures that automatically handle setup/teardown of isolated tmux servers. This eliminates boilerplate and ensures test isolation.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: Session Name Collisions
|
||||
**What goes wrong:** Creating a session with a name that already exists causes tmux error
|
||||
**Why it happens:** tmux requires unique session names per server
|
||||
**How to avoid:** Check `server.sessions.filter(session_name=...)` before creating; append timestamp suffix on collision
|
||||
**Warning signs:** `libtmux.exc.LibTmuxException` on `new_session()`
|
||||
|
||||
### Pitfall 2: State File Corruption
|
||||
**What goes wrong:** Partial writes during crash leave corrupted state.json
|
||||
**Why it happens:** Direct writes without atomic pattern can be interrupted
|
||||
**How to avoid:** Always use temp file + os.replace pattern; keep backup file
|
||||
**Warning signs:** `json.JSONDecodeError` on load; state file exists but session doesn't
|
||||
|
||||
### Pitfall 3: Orphaned tmux Sessions
|
||||
**What goes wrong:** tmux session exists but state.json is missing or corrupted
|
||||
**Why it happens:** External factors (manual tmux commands, system issues) can create sessions
|
||||
**How to avoid:** Startup scan for sessions with `gsd-` prefix without state files; offer cleanup
|
||||
**Warning signs:** `server.sessions` shows sessions not in state directory
|
||||
|
||||
### Pitfall 4: Path Expansion Issues
|
||||
**What goes wrong:** `~` in paths not expanded, or wrong home directory in tests
|
||||
**Why it happens:** String paths vs Path objects; test isolation changes HOME
|
||||
**How to avoid:** Use `Path.home()` for expansion; in tests, use pytest fixtures that set HOME
|
||||
**Warning signs:** FileNotFoundError with literal `~` in path
|
||||
|
||||
## Code Examples
|
||||
|
||||
Verified patterns from official sources:
|
||||
|
||||
### Create Session with libtmux
|
||||
```python
|
||||
# Source: https://github.com/tmux-python/libtmux
|
||||
import libtmux
|
||||
|
||||
def create_session(project_name: str) -> libtmux.Session:
|
||||
server = libtmux.Server()
|
||||
tmux_name = f"gsd-{project_name}"
|
||||
|
||||
# Check for collision and add suffix if needed
|
||||
existing = server.sessions.filter(session_name=tmux_name)
|
||||
if existing:
|
||||
from datetime import datetime
|
||||
suffix = datetime.now().strftime("-%Y%m%d-%H%M%S")
|
||||
tmux_name = f"{tmux_name}{suffix}"
|
||||
|
||||
session = server.new_session(
|
||||
session_name=tmux_name,
|
||||
attach=False,
|
||||
window_name='main'
|
||||
)
|
||||
return session
|
||||
```
|
||||
|
||||
### List Sessions with Project Info
|
||||
```python
|
||||
# Source: https://libtmux.git-pull.com/api/sessions.html
|
||||
def list_gsd_sessions() -> list[dict]:
|
||||
server = libtmux.Server()
|
||||
gsd_sessions = server.sessions.filter(session_name__startswith='gsd-')
|
||||
|
||||
return [
|
||||
{
|
||||
'tmux_name': s.name,
|
||||
'session_id': s.session_id,
|
||||
'windows': len(s.windows),
|
||||
'created': s.session_created,
|
||||
}
|
||||
for s in gsd_sessions
|
||||
]
|
||||
```
|
||||
|
||||
### Atomic State Save with Backup
|
||||
```python
|
||||
# Source: https://blog.gocept.com/2013/07/15/reliable-file-updates-with-python/
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
def save_state_with_backup(state: dict, state_path: Path) -> None:
|
||||
"""Save state atomically, maintaining backup."""
|
||||
state_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Rotate backup if main file exists
|
||||
backup_path = state_path.with_suffix('.json.bak')
|
||||
if state_path.exists():
|
||||
if backup_path.exists():
|
||||
backup_path.unlink()
|
||||
os.replace(state_path, backup_path)
|
||||
|
||||
# Atomic write new state
|
||||
fd, temp_path = tempfile.mkstemp(dir=state_path.parent, suffix='.tmp')
|
||||
try:
|
||||
with os.fdopen(fd, 'w', encoding='utf-8') as f:
|
||||
json.dump(state, f, indent=2, ensure_ascii=False)
|
||||
os.replace(temp_path, state_path)
|
||||
except Exception:
|
||||
if os.path.exists(temp_path):
|
||||
os.unlink(temp_path)
|
||||
raise
|
||||
```
|
||||
|
||||
### State Validation on Load
|
||||
```python
|
||||
def load_and_validate_state(state_path: Path) -> dict | None:
|
||||
"""Load state with validation and fallback to backup."""
|
||||
if not state_path.exists():
|
||||
# Try backup
|
||||
backup_path = state_path.with_suffix('.json.bak')
|
||||
if backup_path.exists():
|
||||
return load_and_validate_state(backup_path)
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(state_path, 'r', encoding='utf-8') as f:
|
||||
state = json.load(f)
|
||||
|
||||
# Validate schema version
|
||||
if state.get('schema_version') != '1.0':
|
||||
raise ValueError(f"Unsupported schema version: {state.get('schema_version')}")
|
||||
|
||||
# Validate required fields
|
||||
required = ['session_id', 'project_name', 'tmux_session_name']
|
||||
missing = [f for f in required if not state.get(f)]
|
||||
if missing:
|
||||
raise ValueError(f"Missing required fields: {missing}")
|
||||
|
||||
return state
|
||||
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
# Try backup on corruption
|
||||
backup_path = state_path.with_suffix('.json.bak')
|
||||
if backup_path.exists() and backup_path != state_path:
|
||||
return load_and_validate_state(backup_path)
|
||||
raise
|
||||
```
|
||||
|
||||
## State of the Art
|
||||
|
||||
| Old Approach | Current Approach | When Changed | Impact |
|
||||
|--------------|------------------|--------------|--------|
|
||||
| subprocess tmux commands | libtmux typed API | libtmux mature since 2020 | Type safety, testability |
|
||||
| Manual pytest setup | libtmux pytest plugin | libtmux 0.30+ (2024) | Auto cleanup, isolated servers |
|
||||
| String paths | pathlib.Path | Python 3.4+ | Cross-platform, chaining |
|
||||
|
||||
**Deprecated/outdated:**
|
||||
- `session.attached_pane` / `session.attached_window`: Use `session.active_pane` / `session.active_window` (deprecated in libtmux 0.31)
|
||||
- `pane.split_window()`: Use `pane.split()` (deprecated in libtmux 0.33)
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Checkpoint Timer Implementation**
|
||||
- What we know: 30-second interval is locked in
|
||||
- What's unclear: Threading vs asyncio vs signal-based timer
|
||||
- Recommendation: Use `threading.Timer` for simplicity; asyncio if already using async elsewhere
|
||||
|
||||
2. **Schema Version Migration**
|
||||
- What we know: Need schema_version field
|
||||
- What's unclear: Migration strategy for future versions
|
||||
- Recommendation: Keep v1 simple; add migration function when v2 schema needed
|
||||
|
||||
## Validation Architecture
|
||||
|
||||
### Test Framework
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Framework | pytest 8.x |
|
||||
| Config file | pyproject.toml (tool.pytest) |
|
||||
| Quick run command | `pytest tests/ -x -q` |
|
||||
| Full suite command | `pytest tests/ -v --cov=src/gsd_openclaw` |
|
||||
|
||||
### Phase Requirements -> Test Map
|
||||
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|
||||
|--------|----------|-----------|-------------------|-------------|
|
||||
| SESS-01 | Create tmux session | integration | `pytest tests/test_tmux_session.py::test_create_session -x` | Wave 0 |
|
||||
| SESS-02 | List active sessions | unit | `pytest tests/test_tmux_session.py::test_list_sessions -x` | Wave 0 |
|
||||
| SESS-03 | Destroy session | integration | `pytest tests/test_tmux_session.py::test_destroy_session -x` | Wave 0 |
|
||||
| SESS-04 | State persistence | unit | `pytest tests/test_state_manager.py::test_save_load_state -x` | Wave 0 |
|
||||
| SESS-05 | Session resume | integration | `pytest tests/test_tmux_session.py::test_resume_session -x` | Wave 0 |
|
||||
| SESS-06 | Workspace isolation | unit | `pytest tests/test_workspace.py::test_workspace_isolation -x` | Wave 0 |
|
||||
| SESS-07 | Concurrent user support | unit | `pytest tests/test_tmux_session.py::test_session_naming_collision -x` | Wave 0 |
|
||||
|
||||
### Sampling Rate
|
||||
- **Per task commit:** `pytest tests/ -x -q` (quick smoke test)
|
||||
- **Per wave merge:** `pytest tests/ -v` (full test suite)
|
||||
- **Phase gate:** Full suite green + coverage report before `/gsd:verify-work`
|
||||
|
||||
### Wave 0 Gaps
|
||||
- [ ] `tests/conftest.py` - libtmux pytest fixtures (server, session, tmp_path)
|
||||
- [ ] `tests/test_state_manager.py` - atomic write, backup, validation tests
|
||||
- [ ] `tests/test_tmux_session.py` - create/list/destroy/resume tests
|
||||
- [ ] `tests/test_workspace.py` - directory structure, cleanup tests
|
||||
- [ ] Framework install: `pip install pytest pytest-cov libtmux` - if none detected
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
- [libtmux GitHub](https://github.com/tmux-python/libtmux) - Official repository with API examples
|
||||
- [libtmux Sessions API](https://libtmux.git-pull.com/api/sessions.html) - Session class documentation
|
||||
- [libtmux Panes API](https://libtmux.git-pull.com/api/panes.html) - Pane class documentation
|
||||
- [libtmux pytest plugin](https://libtmux.git-pull.com/pytest-plugin/) - Test fixtures documentation
|
||||
- [OpenClaw SKILL.md format](https://github.com/openclaw/openclaw/blob/main/skills/skill-creator/SKILL.md) - Official skill structure
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
- [Atomic file updates with Python](https://blog.gocept.com/2013/07/15/reliable-file-updates-with-python/) - Write-replace pattern verified
|
||||
|
||||
### Tertiary (LOW confidence)
|
||||
- None - all critical information verified with primary sources
|
||||
|
||||
## Metadata
|
||||
|
||||
**Confidence breakdown:**
|
||||
- Standard stack: HIGH - libtmux is well-documented, actively maintained, battle-tested via tmuxp
|
||||
- Architecture: HIGH - patterns derived from official libtmux docs and established Python practices
|
||||
- Pitfalls: HIGH - based on libtmux documentation warnings and common tmux issues
|
||||
|
||||
**Research date:** 2026-03-11
|
||||
**Valid until:** 30 days - libtmux API stable, but pre-1.0 (APIs may change in 2026 per docs)
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
phase: 1
|
||||
slug: core-infrastructure
|
||||
status: draft
|
||||
nyquist_compliant: false
|
||||
wave_0_complete: false
|
||||
created: 2026-03-11
|
||||
---
|
||||
|
||||
# Phase 1 — Validation Strategy
|
||||
|
||||
> Per-phase validation contract for feedback sampling during execution.
|
||||
|
||||
---
|
||||
|
||||
## Test Infrastructure
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Framework** | pytest 8.x |
|
||||
| **Config file** | pyproject.toml (tool.pytest) |
|
||||
| **Quick run command** | `pytest tests/ -x -q` |
|
||||
| **Full suite command** | `pytest tests/ -v --cov=src/gsd_openclaw` |
|
||||
| **Estimated runtime** | ~15 seconds |
|
||||
|
||||
---
|
||||
|
||||
## Sampling Rate
|
||||
|
||||
- **After every task commit:** Run `pytest tests/ -x -q`
|
||||
- **After every plan wave:** Run `pytest tests/ -v --cov=src/gsd_openclaw`
|
||||
- **Before `/gsd:verify-work`:** Full suite must be green
|
||||
- **Max feedback latency:** 15 seconds
|
||||
|
||||
---
|
||||
|
||||
## Per-Task Verification Map
|
||||
|
||||
| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status |
|
||||
|---------|------|------|-------------|-----------|-------------------|-------------|--------|
|
||||
| 01-01-01 | 01 | 1 | SESS-01 | integration | `pytest tests/test_tmux_session.py::test_create_session -x` | ❌ W0 | ⬜ pending |
|
||||
| 01-01-02 | 01 | 1 | SESS-02 | unit | `pytest tests/test_tmux_session.py::test_list_sessions -x` | ❌ W0 | ⬜ pending |
|
||||
| 01-01-03 | 01 | 1 | SESS-03 | integration | `pytest tests/test_tmux_session.py::test_destroy_session -x` | ❌ W0 | ⬜ pending |
|
||||
| 01-02-01 | 02 | 1 | SESS-04 | unit | `pytest tests/test_state_manager.py::test_save_load_state -x` | ❌ W0 | ⬜ pending |
|
||||
| 01-02-02 | 02 | 1 | SESS-04 | unit | `pytest tests/test_state_manager.py::test_atomic_write -x` | ❌ W0 | ⬜ pending |
|
||||
| 01-02-03 | 02 | 1 | SESS-04 | unit | `pytest tests/test_state_manager.py::test_backup_rotation -x` | ❌ W0 | ⬜ pending |
|
||||
| 01-03-01 | 03 | 1 | SESS-05 | integration | `pytest tests/test_tmux_session.py::test_resume_session -x` | ❌ W0 | ⬜ pending |
|
||||
| 01-03-02 | 03 | 1 | SESS-05 | unit | `pytest tests/test_state_manager.py::test_state_validation -x` | ❌ W0 | ⬜ pending |
|
||||
| 01-04-01 | 04 | 2 | SESS-06 | unit | `pytest tests/test_workspace.py::test_workspace_isolation -x` | ❌ W0 | ⬜ pending |
|
||||
| 01-04-02 | 04 | 2 | SESS-06 | unit | `pytest tests/test_workspace.py::test_directory_structure -x` | ❌ W0 | ⬜ pending |
|
||||
| 01-05-01 | 05 | 2 | SESS-07 | unit | `pytest tests/test_tmux_session.py::test_session_naming_collision -x` | ❌ W0 | ⬜ pending |
|
||||
| 01-05-02 | 05 | 2 | SESS-07 | integration | `pytest tests/test_tmux_session.py::test_concurrent_sessions -x` | ❌ W0 | ⬜ pending |
|
||||
|
||||
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
||||
|
||||
---
|
||||
|
||||
## Wave 0 Requirements
|
||||
|
||||
- [ ] `tests/conftest.py` — libtmux pytest fixtures (server, session, tmp_path)
|
||||
- [ ] `tests/test_state_manager.py` — atomic write, backup, validation tests
|
||||
- [ ] `tests/test_tmux_session.py` — create/list/destroy/resume tests
|
||||
- [ ] `tests/test_workspace.py` — directory structure, cleanup tests
|
||||
- [ ] `pip install pytest pytest-cov libtmux` — framework install if none detected
|
||||
|
||||
---
|
||||
|
||||
## Manual-Only Verifications
|
||||
|
||||
| Behavior | Requirement | Why Manual | Test Instructions |
|
||||
|----------|-------------|------------|-------------------|
|
||||
| Session persistence across server restart | SESS-04 | Requires full system restart cycle | 1. Create session 2. Restart server 3. Resume and verify state |
|
||||
| Orphan session cleanup | SESS-05 | Requires manual tmux manipulation | 1. Create session via CLI 2. Delete state file 3. Run cleanup and verify |
|
||||
|
||||
---
|
||||
|
||||
## Validation Sign-Off
|
||||
|
||||
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
|
||||
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
|
||||
- [ ] Wave 0 covers all MISSING references
|
||||
- [ ] No watch-mode flags
|
||||
- [ ] Feedback latency < 15s
|
||||
- [ ] `nyquist_compliant: true` set in frontmatter
|
||||
|
||||
**Approval:** pending
|
||||
@@ -0,0 +1,127 @@
|
||||
---
|
||||
phase: 01-core-infrastructure
|
||||
verified: 2026-03-11T12:00:00Z
|
||||
status: passed
|
||||
score: 7/7 must-haves verified
|
||||
gaps: []
|
||||
human_verification:
|
||||
- test: "Run `gsd-ocl create test-project` in a real tmux environment"
|
||||
expected: "Session created with gsd-test-project name, state.json persisted"
|
||||
why_human: "Requires real tmux server running - tests use mocks"
|
||||
- test: "Run `gsd-ocl list` after creating sessions"
|
||||
expected: "Table showing all sessions with project names and status"
|
||||
why_human: "Visual output verification, requires real tmux environment"
|
||||
- test: "Run `gsd-ocl resume test-project` after killing tmux session"
|
||||
expected: "Session recreated with same name, state restored"
|
||||
why_human: "End-to-end resume flow with real tmux"
|
||||
- test: "Run `gsd-ocl cleanup` with orphan sessions"
|
||||
expected: "Orphan sessions killed, results reported"
|
||||
why_human: "End-to-end cleanup flow with real tmux"
|
||||
---
|
||||
|
||||
# Phase 1: Core Infrastructure Verification Report
|
||||
|
||||
**Phase Goal:** Users can create, list, destroy, and resume tmux sessions with persistent state for isolated project workspaces
|
||||
**Verified:** 2026-03-11T12:00:00Z
|
||||
**Status:** passed
|
||||
**Re-verification:** No - initial verification
|
||||
|
||||
## Goal Achievement
|
||||
|
||||
### Observable Truths
|
||||
|
||||
| # | Truth | Status | Evidence |
|
||||
| --- | ----- | ------ | -------- |
|
||||
| 1 | User can create a new tmux session for a project and get a dedicated workspace directory | VERIFIED | `TmuxSessionManager.create_session()` creates tmux session via libtmux, creates workspace via `WorkspaceManager.create()`, persists state via `StateManager.save()` |
|
||||
| 2 | User can list all active sessions with their project associations | VERIFIED | `TmuxSessionManager.list_gsd_sessions()` returns `SessionInfo` with project_name, session_id, created_at, last_active_at, is_orphan |
|
||||
| 3 | User can destroy a session when project is complete, freeing resources | VERIFIED | `TmuxSessionManager.destroy_session()` kills tmux session, removes state files, optionally removes workspace |
|
||||
| 4 | User can resume an interrupted session with full context restoration from JSON checkpoint | VERIFIED | `TmuxSessionManager.resume_session()` validates schema, recreates tmux if missing, updates state with new pane_id |
|
||||
| 5 | Multiple users can run projects concurrently without interference (isolated sessions) | VERIFIED | `WorkspaceManager` uses `~/gsd-workspaces/{project}` per project, `TmuxSession` uses `gsd-{project}` naming with collision handling |
|
||||
|
||||
**Score:** 5/5 truths verified
|
||||
|
||||
### Required Artifacts
|
||||
|
||||
| Artifact | Expected | Status | Details |
|
||||
| -------- | -------- | ------ | ------- |
|
||||
| `src/gsd_openclaw/state_manager.py` | State persistence with atomic writes | VERIFIED | 248 lines, exports SessionState, StateManager, save_state, load_state |
|
||||
| `src/gsd_openclaw/workspace.py` | Workspace directory management | VERIFIED | 144 lines, exports Workspace, WorkspaceManager |
|
||||
| `src/gsd_openclaw/tmux_session.py` | Tmux session lifecycle management | VERIFIED | 468 lines, exports TmuxSession, TmuxSessionManager, SessionInfo, CleanupResult, TmuxSessionError |
|
||||
| `src/gsd_openclaw/cli.py` | CLI entry point | VERIFIED | 135 lines, exports main, provides create/list/destroy/resume/cleanup commands |
|
||||
| `tests/test_state_manager.py` | Test coverage for state operations | VERIFIED | 508 lines, 25 tests |
|
||||
| `tests/test_workspace.py` | Test coverage for workspace operations | VERIFIED | 122 lines, 9 tests |
|
||||
| `tests/test_tmux_session.py` | Test coverage for tmux operations | VERIFIED | 772 lines, 33 tests |
|
||||
| `tests/test_cli.py` | Test coverage for CLI commands | VERIFIED | 310 lines, 16 tests |
|
||||
| `pyproject.toml` | Package configuration with CLI entry point | VERIFIED | gsd-ocl = "gsd_openclaw.cli:main" |
|
||||
|
||||
### Key Link Verification
|
||||
|
||||
| From | To | Via | Status | Details |
|
||||
| ---- | -- | --- | ------ | ------- |
|
||||
| `tmux_session.py` | `libtmux.Server` | `server = server or libtmux.Server()` | WIRED | Line 115, direct libtmux integration |
|
||||
| `TmuxSessionManager` | `StateManager` | `from gsd_openclaw.state_manager import StateManager` | WIRED | Line 17, used in create/destroy/resume/list |
|
||||
| `TmuxSessionManager` | `WorkspaceManager` | `from gsd_openclaw.workspace import WorkspaceManager` | WIRED | Line 18, used in create/destroy/resume |
|
||||
| `cli.py` | `TmuxSessionManager` | `manager = TmuxSessionManager()` | WIRED | Used in all 5 CLI commands (create, list, destroy, resume, cleanup) |
|
||||
| `state_manager.py` | `tempfile + os.replace` | Atomic write pattern | WIRED | Lines 107-112, tempfile.mkstemp + os.replace |
|
||||
| `workspace.py` | `Path.home() / gsd-workspaces` | `WORKSPACES_ROOT = Path.home() / "gsd-workspaces"` | WIRED | Line 85, correct workspace root |
|
||||
|
||||
### Requirements Coverage
|
||||
|
||||
| Requirement | Source Plan | Description | Status | Evidence |
|
||||
| ----------- | ---------- | ----------- | ------ | -------- |
|
||||
| SESS-01 | 01-03 | User can create tmux session for a project workspace | SATISFIED | `TmuxSessionManager.create_session()` creates tmux session, workspace, persists state |
|
||||
| SESS-02 | 01-03 | User can list active tmux sessions with their project associations | SATISFIED | `TmuxSessionManager.list_gsd_sessions()` returns SessionInfo with project associations |
|
||||
| SESS-03 | 01-03 | User can destroy a tmux session when project is complete | SATISFIED | `TmuxSessionManager.destroy_session()` kills session, removes state, optional cleanup |
|
||||
| SESS-04 | 01-01 | Session state persists across server restarts via JSON checkpointing | SATISFIED | `StateManager` with atomic writes, backup rotation, schema validation |
|
||||
| SESS-05 | 01-04 | User can resume interrupted sessions with full context restoration | SATISFIED | `TmuxSessionManager.resume_session()` validates state, recreates tmux if needed |
|
||||
| SESS-06 | 01-02 | Each project has isolated workspace (separate tmux session + directory) | SATISFIED | `Workspace` uses `~/gsd-workspaces/{project}`, `TmuxSession` uses `gsd-{project}` |
|
||||
| SESS-07 | 01-03 | Multiple users can run projects concurrently without interference | SATISFIED | Project isolation via separate workspaces, session naming with collision handling |
|
||||
|
||||
### Anti-Patterns Found
|
||||
|
||||
| File | Line | Pattern | Severity | Impact |
|
||||
| ---- | ---- | ------- | -------- | ------ |
|
||||
| `state_manager.py` | 71 | `# Placeholder functions - to be implemented in Task 2 and 3` | Info | Stale comment - functions are fully implemented. Minor cleanup needed but not blocking. |
|
||||
|
||||
### Human Verification Required
|
||||
|
||||
#### 1. End-to-end session creation
|
||||
**Test:** Run `gsd-ocl create test-project` in a real tmux environment
|
||||
**Expected:** Session created with gsd-test-project name, state.json persisted to ~/gsd-workspaces/test-project/.gsd/state.json
|
||||
**Why human:** Requires real tmux server running - tests use mocks
|
||||
|
||||
#### 2. Session listing display
|
||||
**Test:** Run `gsd-ocl list` after creating sessions
|
||||
**Expected:** Table showing all sessions with project names, session IDs, last active times, and status
|
||||
**Why human:** Visual output verification, requires real tmux environment
|
||||
|
||||
#### 3. Session resume with tmux recreation
|
||||
**Test:** Run `gsd-ocl resume test-project` after killing the tmux session (but keeping state.json)
|
||||
**Expected:** Session recreated with same name, state restored, new pane_id saved
|
||||
**Why human:** End-to-end resume flow with real tmux
|
||||
|
||||
#### 4. Orphan cleanup
|
||||
**Test:** Run `gsd-ocl cleanup` with orphan sessions (tmux sessions without state files)
|
||||
**Expected:** Orphan sessions killed, results reported
|
||||
**Why human:** End-to-end cleanup flow with real tmux
|
||||
|
||||
### Gaps Summary
|
||||
|
||||
No gaps found. All must-haves verified at all three levels (exists, substantive, wired).
|
||||
|
||||
### Test Results
|
||||
|
||||
```
|
||||
83 tests passed in 0.10s
|
||||
- test_state_manager.py: 25 tests
|
||||
- test_workspace.py: 9 tests
|
||||
- test_tmux_session.py: 33 tests
|
||||
- test_cli.py: 16 tests
|
||||
```
|
||||
|
||||
All automated verification passed. Phase goal achieved.
|
||||
|
||||
---
|
||||
|
||||
_Verified: 2026-03-11T12:00:00Z_
|
||||
_Verifier: Claude (gsd-verifier)_
|
||||
Reference in New Issue
Block a user