Merge pull request #312 from AIOSAI/system/devpulse-fixwindows-hooks-use-venv-python-not-python3-307-i
feat(system): fix(windows): hooks use venv python not python3 (#307), identity_injector reads identity.name fallback (#309), Path.rename→os.replace for Windows (#310), RotatingFileHandler catches PermissionError on rotation (#311)
This commit is contained in:
@@ -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')}")
|
||||
|
||||
@@ -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}]}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -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})
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user