--- phase: 04-gsd-integration plan: 01 type: execute wave: 1 depends_on: [] files_modified: - src/gsd_openclaw/user_context.py - src/gsd_openclaw/__init__.py - tests/test_user_context.py - tests/conftest.py autonomous: true requirements: - BRDG-06 user_setup: [] must_haves: truths: - "User's project context persists across chat sessions" - "User context is stored in isolated per-user files" - "User context writes are atomic and crash-safe" - "Each user has independent project tracking" artifacts: - path: "src/gsd_openclaw/user_context.py" provides: "UserContextManager class for per-user project tracking" min_lines: 100 - path: "tests/test_user_context.py" provides: "Tests for UserContextManager" exports: ["TestUserContextManager", "TestAtomicWrites", "TestMultiUser"] key_links: - from: "src/gsd_openclaw/user_context.py" to: "~/gsd-workspaces/.users/{user_id}/context.json" via: "atomic write pattern (tempfile + os.replace)" pattern: "tempfile\\.mkstemp|os\\.replace" --- Create UserContextManager class for per-user project context tracking with atomic persistence. Purpose: Enable users to have persistent project context across chat sessions, supporting the `/gsd:use` command requirement. Output: UserContextManager class with load/save operations, atomic writes, and multi-user isolation. @/home/starmen1265/.claude/get-shit-done/workflows/execute-plan.md @/home/starmen1265/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/04-gsd-integration/04-CONTEXT.md @.planning/phases/04-gsd-integration/04-RESEARCH.md From src/gsd_openclaw/state_manager.py (atomic write pattern to follow): ```python def save_state(state: SessionState, state_path: Path) -> None: """Save session state to file atomically with backup rotation.""" # 1. Create parent directories if missing state_path.parent.mkdir(parents=True, exist_ok=True) # 2. Rotate backup # 3. Write to temp file in same directory fd, temp_path = tempfile.mkstemp(dir=state_path.parent, suffix='.tmp') # 4. Atomic replace os.replace(temp_path, state_path) ``` From src/gsd_openclaw/response_formatter.py: ```python @dataclass class CardButton: """Interactive button for Feishu Card actions.""" text: str value: Dict[str, Any] button_type: str = "primary" # primary, default, danger ``` User context storage location (from CONTEXT.md): - Location: `~/gsd-workspaces/.users/{user_id}/context.json` - Format: JSON, atomic writes (same as Phase 1 state.json) User context data structure (from CONTEXT.md): - current_project: str - User's current project - recent_projects: List[str] - Recent 5 projects - last_active_at: str - ISO timestamp - preferences: dict - Optional settings Task 1: Write UserContext dataclass and UserContextManager interface src/gsd_openclaw/user_context.py, tests/test_user_context.py - Test 1: UserContext can be created with all fields (user_id, current_project, recent_projects, last_active_at, preferences) - Test 2: UserContext.to_dict() returns complete dict with all fields - Test 3: UserContext.from_dict() reconstructs from dict, ignoring unknown fields - Test 4: UserContextManager.__init__ sets users_root and creates context_path correctly - Test 5: UserContextManager.load() returns None for new users (no existing context) - Test 6: UserContextManager.load() returns UserContext for existing context file Create UserContext dataclass and UserContextManager class skeleton in src/gsd_openclaw/user_context.py: 1. Create UserContext dataclass with fields: - schema_version: str = "1.0" - user_id: str = "" - current_project: str = "" - recent_projects: List[str] = field(default_factory=list) - last_active_at: str = "" - preferences: dict = field(default_factory=dict) 2. Add to_dict() and from_dict() methods (follow SessionState pattern exactly) 3. Create UserContextManager class: - USERS_ROOT = Path.home() / "gsd-workspaces" / ".users" - __init__(self, user_id: str, users_root: Optional[Path] = None) - context_path property: users_root / user_id / "context.json" - load() -> Optional[UserContext] method - save(context: UserContext) -> None method (stub for next task) 4. Create tests/test_user_context.py with TestUserContextManager class 5. Follow state_manager.py patterns exactly for consistency pytest tests/test_user_context.py -x -v UserContext dataclass and UserContextManager skeleton exist with passing tests Task 2: Implement atomic save with backup rotation src/gsd_openclaw/user_context.py, tests/test_user_context.py - Test 1: save() creates file atomically (no temp files remain after save) - Test 2: save() creates parent directories if missing - Test 3: save() rotates backup (context.json -> context.json.bak) - Test 4: save() overwrites old backup on subsequent saves - Test 5: save/load roundtrip preserves all data - Test 6: load() falls back to backup when main file is corrupted Implement UserContextManager.save() with atomic write pattern: 1. Follow state_manager.py save_state() pattern exactly: - Create parent directories: context_path.parent.mkdir(parents=True, exist_ok=True) - Rotate backup: if context_path exists, rename to context.json.bak - Write to temp file: tempfile.mkstemp(dir=context_path.parent, suffix='.tmp') - Atomic replace: os.replace(temp_path, context_path) - Exception handling: clean up temp file on failure 2. Add TestAtomicWrites class to tests/test_user_context.py with tests for: - Atomic file creation - Parent directory creation - Backup rotation - Cleanup on failure 3. Ensure load() has backup fallback (same as load_state in state_manager.py) 4. Add imports: tempfile, os, json pytest tests/test_user_context.py::TestAtomicWrites -x -v UserContextManager.save() implements atomic writes with backup rotation, matching state_manager.py pattern Task 3: Implement multi-user isolation and convenience methods src/gsd_openclaw/user_context.py, tests/test_user_context.py, tests/conftest.py, src/gsd_openclaw/__init__.py - Test 1: Two users have completely isolated context files - Test 2: get_current_project() returns current_project or None - Test 3: set_current_project() updates current_project and adds to recent_projects - Test 4: set_current_project() maintains only last 5 recent projects - Test 5: get_recent_projects() returns list of recent projects - Test 6: New user gets default context created on first access Add convenience methods and multi-user support: 1. Add to UserContextManager: - get_current_project() -> Optional[str]: Returns current_project or None - set_current_project(project: str) -> None: * Creates default context if none exists * Updates current_project * Adds to recent_projects (max 5, remove oldest if needed) * Updates last_active_at to now * Calls save() - get_recent_projects() -> List[str]: Returns recent_projects list - create_default_context() -> UserContext: Creates new context with defaults 2. Add TestMultiUser class to tests/test_user_context.py: - Test isolated context paths for different users - Test changes to one user don't affect another 3. Add fixtures to tests/conftest.py: - tmp_users_root(tmp_path): Temp directory for user contexts - user_context_manager(tmp_users_root): UserContextManager with temp root 4. Update src/gsd_openclaw/__init__.py: - Add: from gsd_openclaw.user_context import UserContext, UserContextManager - Add to __all__: "UserContext", "UserContextManager" pytest tests/test_user_context.py::TestMultiUser tests/test_user_context.py::TestUserContextManager -x -v UserContextManager has convenience methods, multi-user isolation works, exports added to __init__.py - All tests in tests/test_user_context.py pass - UserContext follows SessionState pattern for consistency - Atomic write pattern matches state_manager.py exactly - Multi-user isolation verified through tests - UserContextManager class exists with load/save/get_current_project/set_current_project methods - Atomic writes with backup rotation work correctly - Multi-user isolation verified by tests - Exports added to __init__.py After completion, create `.planning/phases/04-gsd-integration/04-01-SUMMARY.md`