Coverage for .claude/hooks/moai/lib/checkpoint.py: 0.00%
60 statements
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-19 08:00 +0900
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-19 08:00 +0900
1#!/usr/bin/env python3
2"""Event-Driven Checkpoint system
4Detect risky tasks and create automatic checkpoints
5"""
7import json
8import re
9import subprocess
10from datetime import datetime
11from pathlib import Path
12from typing import Any
14# Script execution pattern for each language supported by MoAI-ADK
15# Python, TypeScript, Java, Go, Rust, Dart, Swift, Kotlin + Shell
16SCRIPT_EXECUTION_PATTERN = re.compile(
17 r"\b("
18 # Python ecosystem
19 r"python3?|pytest|pip|uv|"
20 # JavaScript/TypeScript ecosystem
21 r"node|npm|npx|yarn|bun|tsx|ts-node|vitest|jest|"
22 # Java ecosystem
23 r"java|javac|mvn|gradle|"
24 # Go
25 r"go|"
26 # Rust
27 r"cargo|"
28 # Dart/Flutter
29 r"dart|flutter|"
30 # Swift
31 r"swift|xcodebuild|"
32 # Kotlin
33 r"kotlinc?|"
34 # Shell scripts and build tools
35 r"bash|sh|zsh|fish|make"
36 r")\b"
37)
40def detect_risky_operation(tool_name: str, tool_args: dict[str, Any], cwd: str) -> tuple[bool, str]:
41 """Risk task detection (for Event-Driven Checkpoint)
43 Claude Code tool automatically detects dangerous tasks before use.
44 When a risk is detected, a checkpoint is automatically created to enable rollback.
46 Args:
47 tool_name: Name of the Claude Code tool (Bash, Edit, Write, MultiEdit)
48 tool_args: Tool argument dictionary
49 cwd: Project root directory path
51 Returns:
52 (is_risky, operation_type) tuple
53 - is_risky: Whether the operation is dangerous (bool)
54 - operation_type: operation type (str: delete, merge, script, critical-file, refactor)
56 Risky Operations:
57 - Bash tool: rm -rf, git merge, git reset --hard, git rebase, script execution
58 - Edit/Write tool: CLAUDE.md, config.json, .claude/skills/*.md
59 - MultiEdit tool: Edit ≥10 items File simultaneously
60 - Script execution: Python, Node, Java, Go, Rust, Dart, Swift, Kotlin, Shell scripts
62 Examples:
63 >>> detect_risky_operation("Bash", {"command": "rm -rf src/"}, ".")
64 (True, 'delete')
65 >>> detect_risky_operation("Edit", {"file_path": "CLAUDE.md"}, ".")
66 (True, 'critical-file')
67 >>> detect_risky_operation("Read", {"file_path": "test.py"}, ".")
68 (False, '')
70 Notes:
71 - Minimize false positives: ignore safe operations
72 - Performance: lightweight string matching (< 1ms)
73 - Extensibility: Easily added to the patterns dictionary
75 """
76 # Bash tool: Detect dangerous commands
77 if tool_name == "Bash":
78 command = tool_args.get("command", "")
80 # Mass Delete
81 if any(pattern in command for pattern in ["rm -rf", "git rm"]):
82 return (True, "delete")
84 # Git merge/reset/rebase
85 if any(pattern in command for pattern in ["git merge", "git reset --hard", "git rebase"]):
86 return (True, "merge")
88 # Execute external script (potentially destructive)
89 if any(command.startswith(prefix) for prefix in ["python ", "node ", "bash ", "sh "]):
90 return (True, "script")
92 # Edit/Write tool: Detect important files
93 if tool_name in ("Edit", "Write"):
94 file_path = tool_args.get("file_path", "")
96 critical_files = [
97 "CLAUDE.md",
98 "config.json",
99 ".claude/skills/moai-core-dev-guide/reference.md",
100 ".claude/skills/moai-core-spec-metadata-extended/reference.md",
101 ".moai/config/config.json",
102 ]
104 if any(cf in file_path for cf in critical_files):
105 return (True, "critical-file")
107 # MultiEdit tool: Detect large edits
108 if tool_name == "MultiEdit":
109 edits = tool_args.get("edits", [])
110 if len(edits) >= 10:
111 return (True, "refactor")
113 return (False, "")
116def create_checkpoint(cwd: str, operation_type: str) -> str:
117 """Create checkpoint (Git local branch)
119 Automatically creates checkpoints before dangerous operations.
120 Prevent remote repository contamination by creating a Git local branch.
122 Args:
123 cwd: Project root directory path
124 operation_type: operation type (delete, merge, script, etc.)
126 Returns:
127 checkpoint_branch: Created branch name
128 Returns "checkpoint-failed" on failure
130 Branch Naming:
131 before-{operation}-{YYYYMMDD-HHMMSS}
132 Example: before-delete-20251015-143000
134 Examples:
135 >>> create_checkpoint(".", "delete")
136 'before-delete-20251015-143000'
138 Notes:
139 - Create only local branch (no remote push)
140 - Fallback in case of Git error (ignore and continue)
141 - Do not check dirty working directory (allow uncommitted changes)
142 - Automatically record checkpoint logs (.moai/checkpoints.log)
144 """
145 timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
146 branch_name = f"before-{operation_type}-{timestamp}"
148 try:
149 # Create a new local branch from the current branch (without checking out)
150 subprocess.run(
151 ["git", "branch", branch_name],
152 cwd=cwd,
153 check=True,
154 capture_output=True,
155 text=True,
156 timeout=2,
157 )
159 # Checkpoint log records
160 log_checkpoint(cwd, branch_name, operation_type)
162 return branch_name
164 except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
165 # Fallback (ignore) in case of Git error
166 return "checkpoint-failed"
169def log_checkpoint(cwd: str, branch_name: str, operation_type: str) -> None:
170 """Checkpoint log records (.moai/checkpoints.log)
172 Checkpoint creation history is recorded in JSON Lines format.
173 SessionStart reads this log to display a list of checkpoints.
175 Args:
176 cwd: Project root directory path
177 branch_name: Created checkpoint branch name
178 operation_type: operation type
180 Log Format (JSON Lines):
181 {"timestamp": "2025-10-15T14:30:00", "branch": "before-delete-...", "operation": "delete"}
183 Examples:
184 >>> log_checkpoint(".", "before-delete-20251015-143000", "delete")
185 # Add 1 line to .moai/checkpoints.log
187 Notes:
188 - If the file does not exist, it is automatically created.
189 - Record in append mode (preserve existing logs)
190 - Ignored in case of failure (not critical)
192 """
193 log_file = Path(cwd) / ".moai" / "checkpoints.log"
195 try:
196 log_file.parent.mkdir(parents=True, exist_ok=True)
198 log_entry = {
199 "timestamp": datetime.now().isoformat(),
200 "branch": branch_name,
201 "operation": operation_type,
202 }
204 with log_file.open("a") as f:
205 f.write(json.dumps(log_entry) + "\n")
207 except (OSError, PermissionError):
208 # Ignore log failures (not critical)
209 pass
212def list_checkpoints(cwd: str, max_count: int = 10) -> list[dict[str, str]]:
213 """Checkpoint list (parsing .moai/checkpoints.log)
215 Returns a list of recently created checkpoints.
216 Used in the SessionStart, /alfred:0-project restore command.
218 Args:
219 cwd: Project root directory path
220 max_count: Maximum number to return (default 10 items)
222 Returns:
223 Checkpoint list (most recent)
224 [{"timestamp": "...", "branch": "...", "operation": "..."}, ...]
226 Examples:
227 >>> list_checkpoints(".")
228 [
229 {"timestamp": "2025-10-15T14:30:00", "branch": "before-delete-...", "operation": "delete"},
230 {"timestamp": "2025-10-15T14:25:00", "branch": "before-merge-...", "operation": "merge"},
231 ]
233 Notes:
234 - If there is no log file, an empty list is returned.
235 - Ignore lines where JSON parsing fails
236 - Return only the latest max_count
238 """
239 log_file = Path(cwd) / ".moai" / "checkpoints.log"
241 if not log_file.exists():
242 return []
244 checkpoints = []
246 try:
247 with log_file.open("r") as f:
248 for line in f:
249 try:
250 checkpoints.append(json.loads(line.strip()))
251 except json.JSONDecodeError:
252 # Ignore lines where parsing failed
253 pass
254 except (OSError, PermissionError):
255 return []
257 # Return only the most recent max_count items (in order of latest)
258 return checkpoints[-max_count:]
261__all__ = [
262 "detect_risky_operation",
263 "create_checkpoint",
264 "log_checkpoint",
265 "list_checkpoints",
266]