diff --git a/.claude/hooks/identity_injector.py b/.claude/hooks/identity_injector.py index f6c7c8bb..10821aaf 100644 --- a/.claude/hooks/identity_injector.py +++ b/.claude/hooks/identity_injector.py @@ -60,8 +60,10 @@ def format_identity(data: dict) -> str: """Format branch_info + identity for injection.""" lines = [] + # Try branch_info first (enriched passports), fall back to identity block (setup.sh passports) branch = data.get("branch_info", {}) - name = branch.get("branch_name", "UNKNOWN") + identity = data.get("identity", {}) + name = branch.get("branch_name") or identity.get("name", "UNKNOWN") lines.append(f"# {name} Identity") lines.append(f"Path: {branch.get('path', 'unknown')}") lines.append(f"Email: {branch.get('email', 'unknown')}") diff --git a/setup.sh b/setup.sh index 25fbaa89..d420a087 100755 --- a/setup.sh +++ b/setup.sh @@ -329,13 +329,21 @@ if [ -d "$SCRIPT_DIR/.claude/hooks" ]; then echo "Installing Claude Code hooks ..." mkdir -p "$HOME/.claude" - python3 - "$SCRIPT_DIR" "$CLAUDE_SETTINGS" << 'PYEOF' + # Determine python command for hooks — venv python on Windows, python3 on Unix + if [ "$IS_WINDOWS" -eq 1 ]; then + HOOK_PYTHON="$SCRIPT_DIR/.venv/Scripts/python.exe" + else + HOOK_PYTHON="python3" + fi + + "$PYTHON" - "$SCRIPT_DIR" "$CLAUDE_SETTINGS" "$HOOK_PYTHON" << 'PYEOF' import json import sys from pathlib import Path repo_root = sys.argv[1] settings_path = Path(sys.argv[2]) +hook_python = sys.argv[3] hooks_dir = f"{repo_root}/.claude/hooks" # Load existing settings or start fresh @@ -348,27 +356,27 @@ else: settings["hooks"] = { "UserPromptSubmit": [ {"hooks": [{"type": "command", "command": f"cat {repo_root}/.aipass/aipass_global_prompt.md 2>/dev/null || true"}]}, - {"hooks": [{"type": "command", "command": f"python3 {hooks_dir}/branch_prompt_loader.py"}]}, - {"hooks": [{"type": "command", "command": f"python3 {hooks_dir}/identity_injector.py"}]}, - {"hooks": [{"type": "command", "command": f"python3 {hooks_dir}/email_notification.py"}]}, + {"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/branch_prompt_loader.py"}]}, + {"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/identity_injector.py"}]}, + {"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/email_notification.py"}]}, ], "PreToolUse": [ {"matcher": "Bash|Edit|MultiEdit|Write|Read|Grep|Glob|WebSearch|WebFetch|Task", - "hooks": [{"type": "command", "command": f"python3 {hooks_dir}/tool_use_sound.py"}]}, + "hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/tool_use_sound.py"}]}, ], "PostToolUse": [ {"matcher": "Edit|MultiEdit|Write|NotebookEdit", - "hooks": [{"type": "command", "command": f"python3 {hooks_dir}/auto_fix_diagnostics.py"}]}, + "hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/auto_fix_diagnostics.py"}]}, ], "Stop": [ - {"hooks": [{"type": "command", "command": f"python3 {hooks_dir}/stop_sound.py"}]}, + {"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/stop_sound.py"}]}, ], "Notification": [ - {"hooks": [{"type": "command", "command": f"python3 {hooks_dir}/notification_sound.py"}]}, + {"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/notification_sound.py"}]}, ], "PreCompact": [ - {"matcher": "manual", "hooks": [{"type": "command", "command": f"python3 {hooks_dir}/pre_compact.py", "timeout": 60}]}, - {"matcher": "auto", "hooks": [{"type": "command", "command": f"python3 {hooks_dir}/pre_compact.py", "timeout": 60}]}, + {"matcher": "manual", "hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/pre_compact.py", "timeout": 60}]}, + {"matcher": "auto", "hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/pre_compact.py", "timeout": 60}]}, ], } @@ -455,14 +463,14 @@ else: # Build hooks config with absolute paths (Gemini uses different event names) settings["hooks"] = { "SessionStart": [ - {"hooks": [{"type": "command", "command": f"python3 {hooks_dir}/session_start_identity.py", "timeout": 10}]} + {"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/session_start_identity.py", "timeout": 10}]} ], "BeforeModel": [ - {"hooks": [{"type": "command", "command": f"python3 {hooks_dir}/prompt_inject.py", "timeout": 10}]} + {"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/prompt_inject.py", "timeout": 10}]} ], "BeforeTool": [ {"matcher": "Edit|Write", - "hooks": [{"type": "command", "command": f"python3 {hooks_dir}/pre_edit_gate.py", "timeout": 5}]} + "hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/pre_edit_gate.py", "timeout": 5}]} ], } diff --git a/src/aipass/memory/apps/handlers/json/memory_files.py b/src/aipass/memory/apps/handlers/json/memory_files.py index 5ea12ad2..dbfe77bb 100644 --- a/src/aipass/memory/apps/handlers/json/memory_files.py +++ b/src/aipass/memory/apps/handlers/json/memory_files.py @@ -27,6 +27,7 @@ Usage: """ import json +import os import tempfile from pathlib import Path from typing import Dict, Any, Optional @@ -152,8 +153,8 @@ def write_memory_file(file_path: Path, data: Dict[str, Any]) -> Dict[str, Any]: json.dump(data, f, indent=2, ensure_ascii=False) f.write('\n') # Add final newline - # Atomic rename (overwrites original) - Path(temp_path).rename(file_path) + # Atomic replace (os.replace overwrites on both Linux and Windows) + os.replace(temp_path, file_path) json_handler.log_operation("write_memory_file", {"file": file_path.name, "success": True}) diff --git a/src/aipass/prax/apps/handlers/logging/setup.py b/src/aipass/prax/apps/handlers/logging/setup.py index df4619e2..092fea72 100755 --- a/src/aipass/prax/apps/handlers/logging/setup.py +++ b/src/aipass/prax/apps/handlers/logging/setup.py @@ -55,6 +55,21 @@ except ImportError as e: create_terminal_handler = None # type: ignore[assignment] should_display_terminal = None # type: ignore[assignment] +class _SafeRotatingHandler(RotatingFileHandler): + """RotatingFileHandler that catches PermissionError during rotation on Windows. + + Windows enforces mandatory file locking — rename fails if another handle + has the file open. On rotation failure, skip the rotation and keep writing + to the current file. Non-fatal. + """ + + def doRollover(self) -> None: + try: + super().doRollover() + except PermissionError: + # Windows: file locked by another process. Skip rotation, keep writing. + pass + def _safe_rotating_handler(log_file: Path, max_bytes: int, backup_count: int) -> logging.Handler: """Create RotatingFileHandler — self-heals missing directories, never crashes.""" try: @@ -63,7 +78,7 @@ def _safe_rotating_handler(log_file: Path, max_bytes: int, backup_count: int) -> parent.mkdir(parents=True, exist_ok=True) if _system_logger: _system_logger.warning(f"Self-healed missing log directory: {parent}") - return RotatingFileHandler(log_file, maxBytes=max_bytes, backupCount=backup_count, encoding='utf-8') + return _SafeRotatingHandler(log_file, maxBytes=max_bytes, backupCount=backup_count, encoding='utf-8') except OSError as e: logger.error("Log handler failed for %s: %s", log_file, e) return logging.NullHandler()