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

1#!/usr/bin/env python3 

2"""Event-Driven Checkpoint system 

3 

4Detect risky tasks and create automatic checkpoints 

5""" 

6 

7import json 

8import re 

9import subprocess 

10from datetime import datetime 

11from pathlib import Path 

12from typing import Any 

13 

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) 

38 

39 

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) 

42 

43 Claude Code tool automatically detects dangerous tasks before use. 

44 When a risk is detected, a checkpoint is automatically created to enable rollback. 

45 

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 

50 

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) 

55 

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 

61 

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, '') 

69 

70 Notes: 

71 - Minimize false positives: ignore safe operations 

72 - Performance: lightweight string matching (< 1ms) 

73 - Extensibility: Easily added to the patterns dictionary 

74 

75 """ 

76 # Bash tool: Detect dangerous commands 

77 if tool_name == "Bash": 

78 command = tool_args.get("command", "") 

79 

80 # Mass Delete 

81 if any(pattern in command for pattern in ["rm -rf", "git rm"]): 

82 return (True, "delete") 

83 

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") 

87 

88 # Execute external script (potentially destructive) 

89 if any(command.startswith(prefix) for prefix in ["python ", "node ", "bash ", "sh "]): 

90 return (True, "script") 

91 

92 # Edit/Write tool: Detect important files 

93 if tool_name in ("Edit", "Write"): 

94 file_path = tool_args.get("file_path", "") 

95 

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 ] 

103 

104 if any(cf in file_path for cf in critical_files): 

105 return (True, "critical-file") 

106 

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") 

112 

113 return (False, "") 

114 

115 

116def create_checkpoint(cwd: str, operation_type: str) -> str: 

117 """Create checkpoint (Git local branch) 

118 

119 Automatically creates checkpoints before dangerous operations. 

120 Prevent remote repository contamination by creating a Git local branch. 

121 

122 Args: 

123 cwd: Project root directory path 

124 operation_type: operation type (delete, merge, script, etc.) 

125 

126 Returns: 

127 checkpoint_branch: Created branch name 

128 Returns "checkpoint-failed" on failure 

129 

130 Branch Naming: 

131 before-{operation}-{YYYYMMDD-HHMMSS} 

132 Example: before-delete-20251015-143000 

133 

134 Examples: 

135 >>> create_checkpoint(".", "delete") 

136 'before-delete-20251015-143000' 

137 

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) 

143 

144 """ 

145 timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") 

146 branch_name = f"before-{operation_type}-{timestamp}" 

147 

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 ) 

158 

159 # Checkpoint log records 

160 log_checkpoint(cwd, branch_name, operation_type) 

161 

162 return branch_name 

163 

164 except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): 

165 # Fallback (ignore) in case of Git error 

166 return "checkpoint-failed" 

167 

168 

169def log_checkpoint(cwd: str, branch_name: str, operation_type: str) -> None: 

170 """Checkpoint log records (.moai/checkpoints.log) 

171 

172 Checkpoint creation history is recorded in JSON Lines format. 

173 SessionStart reads this log to display a list of checkpoints. 

174 

175 Args: 

176 cwd: Project root directory path 

177 branch_name: Created checkpoint branch name 

178 operation_type: operation type 

179 

180 Log Format (JSON Lines): 

181 {"timestamp": "2025-10-15T14:30:00", "branch": "before-delete-...", "operation": "delete"} 

182 

183 Examples: 

184 >>> log_checkpoint(".", "before-delete-20251015-143000", "delete") 

185 # Add 1 line to .moai/checkpoints.log 

186 

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) 

191 

192 """ 

193 log_file = Path(cwd) / ".moai" / "checkpoints.log" 

194 

195 try: 

196 log_file.parent.mkdir(parents=True, exist_ok=True) 

197 

198 log_entry = { 

199 "timestamp": datetime.now().isoformat(), 

200 "branch": branch_name, 

201 "operation": operation_type, 

202 } 

203 

204 with log_file.open("a") as f: 

205 f.write(json.dumps(log_entry) + "\n") 

206 

207 except (OSError, PermissionError): 

208 # Ignore log failures (not critical) 

209 pass 

210 

211 

212def list_checkpoints(cwd: str, max_count: int = 10) -> list[dict[str, str]]: 

213 """Checkpoint list (parsing .moai/checkpoints.log) 

214 

215 Returns a list of recently created checkpoints. 

216 Used in the SessionStart, /alfred:0-project restore command. 

217 

218 Args: 

219 cwd: Project root directory path 

220 max_count: Maximum number to return (default 10 items) 

221 

222 Returns: 

223 Checkpoint list (most recent) 

224 [{"timestamp": "...", "branch": "...", "operation": "..."}, ...] 

225 

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 ] 

232 

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 

237 

238 """ 

239 log_file = Path(cwd) / ".moai" / "checkpoints.log" 

240 

241 if not log_file.exists(): 

242 return [] 

243 

244 checkpoints = [] 

245 

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 [] 

256 

257 # Return only the most recent max_count items (in order of latest) 

258 return checkpoints[-max_count:] 

259 

260 

261__all__ = [ 

262 "detect_risky_operation", 

263 "create_checkpoint", 

264 "log_checkpoint", 

265 "list_checkpoints", 

266]