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:
AIPass
2026-04-16 01:01:06 -07:00
committed by GitHub
4 changed files with 43 additions and 17 deletions
+3 -1
View File
@@ -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')}")
+21 -13
View File
@@ -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})
+16 -1
View File
@@ -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()