moai-adk 0.10.1__py3-none-any.whl → 0.11.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of moai-adk might be problematic. Click here for more details.

Files changed (58) hide show
  1. moai_adk/core/issue_creator.py +2 -2
  2. moai_adk/core/project/detector.py +285 -12
  3. moai_adk/core/project/phase_executor.py +4 -0
  4. moai_adk/core/tags/ci_validator.py +33 -3
  5. moai_adk/core/template_engine.py +6 -2
  6. moai_adk/templates/.claude/commands/alfred/0-project.md +60 -62
  7. moai_adk/templates/.claude/commands/alfred/1-plan.md +6 -0
  8. moai_adk/templates/.claude/commands/alfred/2-run.md +6 -0
  9. moai_adk/templates/.claude/commands/alfred/3-sync.md +6 -0
  10. moai_adk/templates/.claude/hooks/alfred/alfred_hooks.py +8 -9
  11. moai_adk/templates/.claude/hooks/alfred/core/project.py +22 -28
  12. moai_adk/templates/.claude/hooks/alfred/core/timeout.py +136 -0
  13. moai_adk/templates/.claude/hooks/alfred/core/ttl_cache.py +109 -0
  14. moai_adk/templates/.claude/hooks/alfred/core/version_cache.py +4 -4
  15. moai_adk/templates/.claude/hooks/alfred/notification__handle_events.py +10 -15
  16. moai_adk/templates/.claude/hooks/alfred/post_tool__log_changes.py +10 -15
  17. moai_adk/templates/.claude/hooks/alfred/pre_tool__auto_checkpoint.py +10 -15
  18. moai_adk/templates/.claude/hooks/alfred/session_end__cleanup.py +10 -15
  19. moai_adk/templates/.claude/hooks/alfred/session_start__show_project_info.py +10 -15
  20. moai_adk/templates/.claude/hooks/alfred/shared/core/__init__.py +2 -2
  21. moai_adk/templates/.claude/hooks/alfred/shared/core/project.py +19 -26
  22. moai_adk/templates/.claude/hooks/alfred/shared/core/tags.py +55 -23
  23. moai_adk/templates/.claude/hooks/alfred/shared/core/version_cache.py +4 -4
  24. moai_adk/templates/.claude/hooks/alfred/shared/handlers/notification.py +134 -3
  25. moai_adk/templates/.claude/hooks/alfred/shared/handlers/session.py +9 -10
  26. moai_adk/templates/.claude/hooks/alfred/shared/handlers/tool.py +3 -6
  27. moai_adk/templates/.claude/hooks/alfred/stop__handle_interrupt.py +10 -15
  28. moai_adk/templates/.claude/hooks/alfred/subagent_stop__handle_subagent_end.py +10 -15
  29. moai_adk/templates/.claude/hooks/alfred/user_prompt__jit_load_docs.py +11 -20
  30. moai_adk/templates/.claude/hooks/alfred/utils/__init__.py +1 -0
  31. moai_adk/templates/.claude/hooks/alfred/utils/timeout.py +136 -0
  32. moai_adk/templates/.github/workflows/c-tag-validation.yml +83 -0
  33. moai_adk/templates/.github/workflows/cpp-tag-validation.yml +79 -0
  34. moai_adk/templates/.github/workflows/csharp-tag-validation.yml +65 -0
  35. moai_adk/templates/.github/workflows/dart-tag-validation.yml +82 -0
  36. moai_adk/templates/.github/workflows/java-tag-validation.yml +75 -0
  37. moai_adk/templates/.github/workflows/kotlin-tag-validation.yml +67 -0
  38. moai_adk/templates/.github/workflows/{release.yml → moai-adk-release.yml} +6 -2
  39. moai_adk/templates/.github/workflows/{tag-validation.yml → moai-adk-tag-validation.yml} +53 -8
  40. moai_adk/templates/.github/workflows/moai-gitflow.yml +6 -1
  41. moai_adk/templates/.github/workflows/php-tag-validation.yml +56 -0
  42. moai_adk/templates/.github/workflows/ruby-tag-validation.yml +68 -0
  43. moai_adk/templates/.github/workflows/rust-tag-validation.yml +73 -0
  44. moai_adk/templates/.github/workflows/shell-tag-validation.yml +65 -0
  45. moai_adk/templates/.github/workflows/swift-tag-validation.yml +79 -0
  46. moai_adk/templates/.moai/memory/GITFLOW-PROTECTION-POLICY.md +330 -0
  47. moai_adk/templates/.moai/memory/SPEC-METADATA.md +356 -0
  48. moai_adk/templates/CLAUDE.md +536 -65
  49. moai_adk/templates/workflows/go-tag-validation.yml +130 -0
  50. moai_adk/templates/workflows/javascript-tag-validation.yml +135 -0
  51. moai_adk/templates/workflows/python-tag-validation.yml +118 -0
  52. moai_adk/templates/workflows/typescript-tag-validation.yml +154 -0
  53. {moai_adk-0.10.1.dist-info → moai_adk-0.11.1.dist-info}/METADATA +70 -13
  54. {moai_adk-0.10.1.dist-info → moai_adk-0.11.1.dist-info}/RECORD +58 -37
  55. /moai_adk/templates/.github/workflows/{spec-issue-sync.yml → moai-adk-spec-issue-sync.yml} +0 -0
  56. {moai_adk-0.10.1.dist-info → moai_adk-0.11.1.dist-info}/WHEEL +0 -0
  57. {moai_adk-0.10.1.dist-info → moai_adk-0.11.1.dist-info}/entry_points.txt +0 -0
  58. {moai_adk-0.10.1.dist-info → moai_adk-0.11.1.dist-info}/licenses/LICENSE +0 -0
@@ -16,9 +16,10 @@ Risky Operations Detected:
16
16
  """
17
17
 
18
18
  import json
19
- import signal
20
19
  import sys
21
- from pathlib import Path
20
+ from pathlib import
21
+ from utils.timeout import CrossPlatformTimeout, TimeoutError as PlatformTimeoutError
22
+ Path
22
23
  from typing import Any
23
24
 
24
25
  # Setup import path for shared modules
@@ -30,15 +31,9 @@ if str(SHARED_DIR) not in sys.path:
30
31
  from handlers import handle_pre_tool_use
31
32
 
32
33
 
33
- class HookTimeoutError(Exception):
34
- """Hook execution timeout exception"""
35
34
  pass
36
35
 
37
36
 
38
- def _timeout_handler(signum, frame):
39
- """Signal handler for 5-second timeout"""
40
- raise HookTimeoutError("Hook execution exceeded 5-second timeout")
41
-
42
37
 
43
38
  def main() -> None:
44
39
  """Main entry point for PreToolUse hook
@@ -54,8 +49,8 @@ def main() -> None:
54
49
  1: Error (timeout, JSON parse failure, handler exception)
55
50
  """
56
51
  # Set 5-second timeout
57
- signal.signal(signal.SIGALRM, _timeout_handler)
58
- signal.alarm(5)
52
+ timeout = CrossPlatformTimeout(5)
53
+ timeout.start()
59
54
 
60
55
  try:
61
56
  # Read JSON payload from stdin
@@ -69,11 +64,11 @@ def main() -> None:
69
64
  print(json.dumps(result.to_dict()))
70
65
  sys.exit(0)
71
66
 
72
- except HookTimeoutError:
67
+ except PlatformTimeoutError:
73
68
  # Timeout - return minimal valid response (allow operation to continue)
74
69
  timeout_response: dict[str, Any] = {
75
70
  "continue": True,
76
- "systemMessage": "⚠️ Checkpoint creation timeout - operation proceeding without checkpoint"
71
+ "systemMessage": "⚠️ Checkpoint creation timeout - operation proceeding without checkpoint",
77
72
  }
78
73
  print(json.dumps(timeout_response))
79
74
  print("PreToolUse hook timeout after 5 seconds", file=sys.stderr)
@@ -83,7 +78,7 @@ def main() -> None:
83
78
  # JSON parse error - allow operation to continue
84
79
  error_response: dict[str, Any] = {
85
80
  "continue": True,
86
- "hookSpecificOutput": {"error": f"JSON parse error: {e}"}
81
+ "hookSpecificOutput": {"error": f"JSON parse error: {e}"},
87
82
  }
88
83
  print(json.dumps(error_response))
89
84
  print(f"PreToolUse JSON parse error: {e}", file=sys.stderr)
@@ -93,7 +88,7 @@ def main() -> None:
93
88
  # Unexpected error - allow operation to continue
94
89
  error_response: dict[str, Any] = {
95
90
  "continue": True,
96
- "hookSpecificOutput": {"error": f"PreToolUse error: {e}"}
91
+ "hookSpecificOutput": {"error": f"PreToolUse error: {e}"},
97
92
  }
98
93
  print(json.dumps(error_response))
99
94
  print(f"PreToolUse unexpected error: {e}", file=sys.stderr)
@@ -101,7 +96,7 @@ def main() -> None:
101
96
 
102
97
  finally:
103
98
  # Always cancel alarm
104
- signal.alarm(0)
99
+ timeout.cancel()
105
100
 
106
101
 
107
102
  if __name__ == "__main__":
@@ -10,9 +10,10 @@ Output: Continue execution (currently a stub for future enhancements)
10
10
  """
11
11
 
12
12
  import json
13
- import signal
14
13
  import sys
15
- from pathlib import Path
14
+ from pathlib import
15
+ from utils.timeout import CrossPlatformTimeout, TimeoutError as PlatformTimeoutError
16
+ Path
16
17
  from typing import Any
17
18
 
18
19
  # Setup import path for shared modules
@@ -24,15 +25,9 @@ if str(SHARED_DIR) not in sys.path:
24
25
  from handlers import handle_session_end
25
26
 
26
27
 
27
- class HookTimeoutError(Exception):
28
- """Hook execution timeout exception"""
29
28
  pass
30
29
 
31
30
 
32
- def _timeout_handler(signum, frame):
33
- """Signal handler for 5-second timeout"""
34
- raise HookTimeoutError("Hook execution exceeded 5-second timeout")
35
-
36
31
 
37
32
  def main() -> None:
38
33
  """Main entry point for SessionEnd hook
@@ -48,8 +43,8 @@ def main() -> None:
48
43
  1: Error (timeout, JSON parse failure, handler exception)
49
44
  """
50
45
  # Set 5-second timeout
51
- signal.signal(signal.SIGALRM, _timeout_handler)
52
- signal.alarm(5)
46
+ timeout = CrossPlatformTimeout(5)
47
+ timeout.start()
53
48
 
54
49
  try:
55
50
  # Read JSON payload from stdin
@@ -63,11 +58,11 @@ def main() -> None:
63
58
  print(json.dumps(result.to_dict()))
64
59
  sys.exit(0)
65
60
 
66
- except HookTimeoutError:
61
+ except PlatformTimeoutError:
67
62
  # Timeout - return minimal valid response
68
63
  timeout_response: dict[str, Any] = {
69
64
  "continue": True,
70
- "systemMessage": "⚠️ SessionEnd cleanup timeout - session ending anyway"
65
+ "systemMessage": "⚠️ SessionEnd cleanup timeout - session ending anyway",
71
66
  }
72
67
  print(json.dumps(timeout_response))
73
68
  print("SessionEnd hook timeout after 5 seconds", file=sys.stderr)
@@ -77,7 +72,7 @@ def main() -> None:
77
72
  # JSON parse error
78
73
  error_response: dict[str, Any] = {
79
74
  "continue": True,
80
- "hookSpecificOutput": {"error": f"JSON parse error: {e}"}
75
+ "hookSpecificOutput": {"error": f"JSON parse error: {e}"},
81
76
  }
82
77
  print(json.dumps(error_response))
83
78
  print(f"SessionEnd JSON parse error: {e}", file=sys.stderr)
@@ -87,7 +82,7 @@ def main() -> None:
87
82
  # Unexpected error
88
83
  error_response: dict[str, Any] = {
89
84
  "continue": True,
90
- "hookSpecificOutput": {"error": f"SessionEnd error: {e}"}
85
+ "hookSpecificOutput": {"error": f"SessionEnd error: {e}"},
91
86
  }
92
87
  print(json.dumps(error_response))
93
88
  print(f"SessionEnd unexpected error: {e}", file=sys.stderr)
@@ -95,7 +90,7 @@ def main() -> None:
95
90
 
96
91
  finally:
97
92
  # Always cancel alarm
98
- signal.alarm(0)
93
+ timeout.cancel()
99
94
 
100
95
 
101
96
  if __name__ == "__main__":
@@ -10,9 +10,10 @@ Output: System message with formatted project summary
10
10
  """
11
11
 
12
12
  import json
13
- import signal
14
13
  import sys
15
- from pathlib import Path
14
+ from pathlib import
15
+ from utils.timeout import CrossPlatformTimeout, TimeoutError as PlatformTimeoutError
16
+ Path
16
17
  from typing import Any
17
18
 
18
19
  # Setup import path for shared modules
@@ -24,15 +25,9 @@ if str(SHARED_DIR) not in sys.path:
24
25
  from handlers import handle_session_start
25
26
 
26
27
 
27
- class HookTimeoutError(Exception):
28
- """Hook execution timeout exception"""
29
28
  pass
30
29
 
31
30
 
32
- def _timeout_handler(signum, frame):
33
- """Signal handler for 5-second timeout"""
34
- raise HookTimeoutError("Hook execution exceeded 5-second timeout")
35
-
36
31
 
37
32
  def main() -> None:
38
33
  """Main entry point for SessionStart hook
@@ -48,8 +43,8 @@ def main() -> None:
48
43
  1: Error (timeout, JSON parse failure, handler exception)
49
44
  """
50
45
  # Set 5-second timeout
51
- signal.signal(signal.SIGALRM, _timeout_handler)
52
- signal.alarm(5)
46
+ timeout = CrossPlatformTimeout(5)
47
+ timeout.start()
53
48
 
54
49
  try:
55
50
  # Read JSON payload from stdin
@@ -63,11 +58,11 @@ def main() -> None:
63
58
  print(json.dumps(result.to_dict()))
64
59
  sys.exit(0)
65
60
 
66
- except HookTimeoutError:
61
+ except PlatformTimeoutError:
67
62
  # Timeout - return minimal valid response
68
63
  timeout_response: dict[str, Any] = {
69
64
  "continue": True,
70
- "systemMessage": "⚠️ Session start timeout - continuing without project info"
65
+ "systemMessage": "⚠️ Session start timeout - continuing without project info",
71
66
  }
72
67
  print(json.dumps(timeout_response))
73
68
  print("SessionStart hook timeout after 5 seconds", file=sys.stderr)
@@ -77,7 +72,7 @@ def main() -> None:
77
72
  # JSON parse error
78
73
  error_response: dict[str, Any] = {
79
74
  "continue": True,
80
- "hookSpecificOutput": {"error": f"JSON parse error: {e}"}
75
+ "hookSpecificOutput": {"error": f"JSON parse error: {e}"},
81
76
  }
82
77
  print(json.dumps(error_response))
83
78
  print(f"SessionStart JSON parse error: {e}", file=sys.stderr)
@@ -87,7 +82,7 @@ def main() -> None:
87
82
  # Unexpected error
88
83
  error_response: dict[str, Any] = {
89
84
  "continue": True,
90
- "hookSpecificOutput": {"error": f"SessionStart error: {e}"}
85
+ "hookSpecificOutput": {"error": f"SessionStart error: {e}"},
91
86
  }
92
87
  print(json.dumps(error_response))
93
88
  print(f"SessionStart unexpected error: {e}", file=sys.stderr)
@@ -95,7 +90,7 @@ def main() -> None:
95
90
 
96
91
  finally:
97
92
  # Always cancel alarm
98
- signal.alarm(0)
93
+ timeout.cancel()
99
94
 
100
95
 
101
96
  if __name__ == "__main__":
@@ -156,8 +156,8 @@ class HookResult:
156
156
  "continue": self.continue_execution,
157
157
  "hookSpecificOutput": {
158
158
  "hookEventName": "UserPromptSubmit",
159
- "additionalContext": context_str
160
- }
159
+ "additionalContext": context_str,
160
+ },
161
161
  }
162
162
 
163
163
 
@@ -70,6 +70,7 @@ def find_project_root(start_path: str | Path = ".") -> Path:
70
70
 
71
71
  class TimeoutError(Exception):
72
72
  """Signal-based timeout exception"""
73
+
73
74
  pass
74
75
 
75
76
 
@@ -86,6 +87,7 @@ def timeout_handler(seconds: int):
86
87
  Raises:
87
88
  TimeoutError: If operation exceeds timeout
88
89
  """
90
+
89
91
  def _handle_timeout(signum, frame):
90
92
  raise TimeoutError(f"Operation timed out after {seconds} seconds")
91
93
 
@@ -427,19 +429,10 @@ def get_version_check_config(cwd: str) -> dict[str, Any]:
427
429
  - REFACTOR: Add validation and error handling
428
430
  """
429
431
  # TTL mapping by frequency
430
- TTL_BY_FREQUENCY = {
431
- "always": 0,
432
- "daily": 24,
433
- "weekly": 168,
434
- "never": float('inf')
435
- }
432
+ ttl_by_frequency = {"always": 0, "daily": 24, "weekly": 168, "never": float("inf")}
436
433
 
437
434
  # Default configuration
438
- defaults = {
439
- "enabled": True,
440
- "frequency": "daily",
441
- "cache_ttl_hours": 24
442
- }
435
+ defaults = {"enabled": True, "frequency": "daily", "cache_ttl_hours": 24}
443
436
 
444
437
  # Find project root to ensure we read config from correct location
445
438
  project_root = find_project_root(cwd)
@@ -461,21 +454,17 @@ def get_version_check_config(cwd: str) -> dict[str, Any]:
461
454
  frequency = moai_config.get("update_check_frequency", defaults["frequency"])
462
455
 
463
456
  # Validate frequency
464
- if frequency not in TTL_BY_FREQUENCY:
457
+ if frequency not in ttl_by_frequency:
465
458
  frequency = defaults["frequency"]
466
459
 
467
460
  # Calculate TTL from frequency
468
- cache_ttl_hours = TTL_BY_FREQUENCY[frequency]
461
+ cache_ttl_hours = ttl_by_frequency[frequency]
469
462
 
470
463
  # Allow explicit cache_ttl_hours override
471
464
  if "cache_ttl_hours" in version_check_config:
472
465
  cache_ttl_hours = version_check_config["cache_ttl_hours"]
473
466
 
474
- return {
475
- "enabled": enabled,
476
- "frequency": frequency,
477
- "cache_ttl_hours": cache_ttl_hours
478
- }
467
+ return {"enabled": enabled, "frequency": frequency, "cache_ttl_hours": cache_ttl_hours}
479
468
 
480
469
  except (OSError, json.JSONDecodeError, KeyError):
481
470
  # Config read or parse error - return defaults
@@ -619,13 +608,13 @@ def get_package_version_info(cwd: str = ".") -> dict[str, Any]:
619
608
  if spec and spec.loader:
620
609
  version_cache_module = importlib.util.module_from_spec(spec)
621
610
  spec.loader.exec_module(version_cache_module)
622
- VersionCache = version_cache_module.VersionCache
611
+ version_cache_class = version_cache_module.VersionCache
623
612
  else:
624
613
  # Skip caching if module can't be loaded
625
- VersionCache = None
614
+ version_cache_class = None
626
615
  except (ImportError, OSError):
627
616
  # Graceful degradation: skip caching on import errors
628
- VersionCache = None
617
+ version_cache_class = None
629
618
 
630
619
  # 1. Find project root (ensure cache is always in correct location)
631
620
  # This prevents creating .moai/cache in wrong locations when hooks run
@@ -634,7 +623,7 @@ def get_package_version_info(cwd: str = ".") -> dict[str, Any]:
634
623
 
635
624
  # 2. Initialize cache (skip if VersionCache couldn't be imported)
636
625
  cache_dir = project_root / CACHE_DIR_NAME
637
- version_cache = VersionCache(cache_dir) if VersionCache else None
626
+ version_cache = version_cache_class(cache_dir) if version_cache_class else None
638
627
 
639
628
  # 2. Get current installed version first (needed for cache validation)
640
629
  current_version = "unknown"
@@ -647,7 +636,7 @@ def get_package_version_info(cwd: str = ".") -> dict[str, Any]:
647
636
  "current": "dev",
648
637
  "latest": "unknown",
649
638
  "update_available": False,
650
- "upgrade_command": ""
639
+ "upgrade_command": "",
651
640
  }
652
641
 
653
642
  # 3. Try to load from cache (fast path with version validation)
@@ -670,7 +659,7 @@ def get_package_version_info(cwd: str = ".") -> dict[str, Any]:
670
659
  "current": current_version,
671
660
  "latest": "unknown",
672
661
  "update_available": False,
673
- "upgrade_command": ""
662
+ "upgrade_command": "",
674
663
  }
675
664
 
676
665
  # 5. Check if version check is enabled in config
@@ -701,7 +690,9 @@ def get_package_version_info(cwd: str = ".") -> dict[str, Any]:
701
690
  release_url = project_urls.get("Changelog", "")
702
691
  if not release_url:
703
692
  # Fallback to GitHub releases URL pattern
704
- release_url = f"https://github.com/modu-ai/moai-adk/releases/tag/v{result['latest']}"
693
+ release_url = (
694
+ f"https://github.com/modu-ai/moai-adk/releases/tag/v{result['latest']}"
695
+ )
705
696
  result["release_notes_url"] = release_url
706
697
  except (KeyError, AttributeError, TypeError):
707
698
  result["release_notes_url"] = None
@@ -728,7 +719,9 @@ def get_package_version_info(cwd: str = ".") -> dict[str, Any]:
728
719
  result["upgrade_command"] = f"uv pip install --upgrade moai-adk>={result['latest']}"
729
720
 
730
721
  # Detect major version change
731
- result["is_major_update"] = is_major_version_change(result["current"], result["latest"])
722
+ result["is_major_update"] = is_major_version_change(
723
+ result["current"], result["latest"]
724
+ )
732
725
  else:
733
726
  result["is_major_update"] = False
734
727
  except (ValueError, AttributeError):
@@ -38,9 +38,22 @@ from pathlib import Path
38
38
  from typing import Iterable, List, Optional
39
39
 
40
40
  DEFAULT_CODE_EXTS = (
41
- ".py", ".ts", ".tsx", ".js", ".jsx", ".go", ".rs",
42
- ".java", ".kt", ".rb", ".php", ".c", ".cpp", ".cs",
43
- ".swift", ".scala"
41
+ ".py",
42
+ ".ts",
43
+ ".tsx",
44
+ ".js",
45
+ ".jsx",
46
+ ".go",
47
+ ".rs",
48
+ ".java",
49
+ ".kt",
50
+ ".rb",
51
+ ".php",
52
+ ".c",
53
+ ".cpp",
54
+ ".cs",
55
+ ".swift",
56
+ ".scala",
44
57
  )
45
58
 
46
59
 
@@ -88,35 +101,49 @@ def _load_rules(cwd: str) -> List[Rule]:
88
101
 
89
102
  # Defaults (ordered)
90
103
  return [
91
- Rule(
92
- include=[".moai/specs/**", "**/SPEC-*/spec.md"],
93
- expect="@SPEC:",
94
- exclude=[]
95
- ),
104
+ Rule(include=[".moai/specs/**", "**/SPEC-*/spec.md"], expect="@SPEC:", exclude=[]),
96
105
  Rule(
97
106
  include=[
98
- "**/*_test.py", "**/test_*.py", "**/*.test.ts",
99
- "**/*.test.tsx", "**/*.test.js", "**/*.test.jsx",
100
- "**/*.test.go", "**/*.test.rs", "**/*.spec.ts",
101
- "**/*.spec.tsx", "tests/**"
107
+ "**/*_test.py",
108
+ "**/test_*.py",
109
+ "**/*.test.ts",
110
+ "**/*.test.tsx",
111
+ "**/*.test.js",
112
+ "**/*.test.jsx",
113
+ "**/*.test.go",
114
+ "**/*.test.rs",
115
+ "**/*.spec.ts",
116
+ "**/*.spec.tsx",
117
+ "tests/**",
102
118
  ],
103
119
  expect="@TEST:",
104
- exclude=[".claude/**"]
120
+ exclude=[".claude/**"],
105
121
  ),
106
122
  Rule(
107
123
  include=["docs/**/*.md", "**/README.md", "**/*.api.md"],
108
124
  expect="@DOC:",
109
- exclude=[".claude/**"]
125
+ exclude=[".claude/**"],
110
126
  ),
111
127
  Rule(
112
128
  include=["**/*"],
113
129
  expect="@CODE:",
114
130
  exclude=[
115
- "tests/**", "docs/**", ".moai/**", ".claude/**",
116
- "**/*.md", "**/*.json", "**/*.yml", "**/*.yaml",
117
- "**/*.toml", "**/*.lock", "**/*.svg", "**/*.png",
118
- "**/*.jpg", "**/*.jpeg", "**/*.gif"
119
- ]
131
+ "tests/**",
132
+ "docs/**",
133
+ ".moai/**",
134
+ ".claude/**",
135
+ "**/*.md",
136
+ "**/*.json",
137
+ "**/*.yml",
138
+ "**/*.yaml",
139
+ "**/*.toml",
140
+ "**/*.lock",
141
+ "**/*.svg",
142
+ "**/*.png",
143
+ "**/*.jpg",
144
+ "**/*.jpeg",
145
+ "**/*.gif",
146
+ ],
120
147
  ),
121
148
  ]
122
149
 
@@ -147,17 +174,22 @@ def _iter_recent_changes(cwd: str) -> Iterable[Path]:
147
174
  # Staged files
148
175
  r1 = subprocess.run(
149
176
  ["git", "diff", "--name-only", "--cached"],
150
- cwd=cwd, capture_output=True, text=True, timeout=1
177
+ cwd=cwd,
178
+ capture_output=True,
179
+ text=True,
180
+ timeout=1,
151
181
  )
152
182
  # Modified (unstaged) tracked files
153
183
  r2 = subprocess.run(
154
- ["git", "ls-files", "-m"],
155
- cwd=cwd, capture_output=True, text=True, timeout=1
184
+ ["git", "ls-files", "-m"], cwd=cwd, capture_output=True, text=True, timeout=1
156
185
  )
157
186
  # Untracked (other) files respecting .gitignore
158
187
  r3 = subprocess.run(
159
188
  ["git", "ls-files", "-o", "--exclude-standard"],
160
- cwd=cwd, capture_output=True, text=True, timeout=1
189
+ cwd=cwd,
190
+ capture_output=True,
191
+ text=True,
192
+ timeout=1,
161
193
  )
162
194
  names = set()
163
195
  if r1.returncode == 0:
@@ -86,7 +86,7 @@ class VersionCache:
86
86
  return False
87
87
 
88
88
  try:
89
- with open(self.cache_file, 'r') as f:
89
+ with open(self.cache_file, "r") as f:
90
90
  data = json.load(f)
91
91
 
92
92
  age_hours = self._calculate_age_hours(data["last_check"])
@@ -112,7 +112,7 @@ class VersionCache:
112
112
  return None
113
113
 
114
114
  try:
115
- with open(self.cache_file, 'r') as f:
115
+ with open(self.cache_file, "r") as f:
116
116
  return json.load(f)
117
117
  except (json.JSONDecodeError, OSError):
118
118
  # Graceful degradation on read errors
@@ -144,7 +144,7 @@ class VersionCache:
144
144
  version_info["last_check"] = datetime.now(timezone.utc).isoformat()
145
145
 
146
146
  # Write to cache file
147
- with open(self.cache_file, 'w') as f:
147
+ with open(self.cache_file, "w") as f:
148
148
  json.dump(version_info, f, indent=2)
149
149
 
150
150
  return True
@@ -186,7 +186,7 @@ class VersionCache:
186
186
  return 0.0
187
187
 
188
188
  try:
189
- with open(self.cache_file, 'r') as f:
189
+ with open(self.cache_file, "r") as f:
190
190
  data = json.load(f)
191
191
 
192
192
  return self._calculate_age_hours(data["last_check"])