crackerjack 0.32.0__py3-none-any.whl → 0.33.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 crackerjack might be problematic. Click here for more details.

Files changed (200) hide show
  1. crackerjack/__main__.py +1350 -34
  2. crackerjack/adapters/__init__.py +17 -0
  3. crackerjack/adapters/lsp_client.py +358 -0
  4. crackerjack/adapters/rust_tool_adapter.py +194 -0
  5. crackerjack/adapters/rust_tool_manager.py +193 -0
  6. crackerjack/adapters/skylos_adapter.py +231 -0
  7. crackerjack/adapters/zuban_adapter.py +560 -0
  8. crackerjack/agents/base.py +7 -3
  9. crackerjack/agents/coordinator.py +271 -33
  10. crackerjack/agents/documentation_agent.py +9 -15
  11. crackerjack/agents/dry_agent.py +3 -15
  12. crackerjack/agents/formatting_agent.py +1 -1
  13. crackerjack/agents/import_optimization_agent.py +36 -180
  14. crackerjack/agents/performance_agent.py +17 -98
  15. crackerjack/agents/performance_helpers.py +7 -31
  16. crackerjack/agents/proactive_agent.py +1 -3
  17. crackerjack/agents/refactoring_agent.py +16 -85
  18. crackerjack/agents/refactoring_helpers.py +7 -42
  19. crackerjack/agents/security_agent.py +9 -48
  20. crackerjack/agents/test_creation_agent.py +356 -513
  21. crackerjack/agents/test_specialist_agent.py +0 -4
  22. crackerjack/api.py +6 -25
  23. crackerjack/cli/cache_handlers.py +204 -0
  24. crackerjack/cli/cache_handlers_enhanced.py +683 -0
  25. crackerjack/cli/facade.py +100 -0
  26. crackerjack/cli/handlers.py +224 -9
  27. crackerjack/cli/interactive.py +6 -4
  28. crackerjack/cli/options.py +642 -55
  29. crackerjack/cli/utils.py +2 -1
  30. crackerjack/code_cleaner.py +58 -117
  31. crackerjack/config/global_lock_config.py +8 -48
  32. crackerjack/config/hooks.py +53 -62
  33. crackerjack/core/async_workflow_orchestrator.py +24 -34
  34. crackerjack/core/autofix_coordinator.py +3 -17
  35. crackerjack/core/enhanced_container.py +64 -6
  36. crackerjack/core/file_lifecycle.py +12 -89
  37. crackerjack/core/performance.py +2 -2
  38. crackerjack/core/performance_monitor.py +15 -55
  39. crackerjack/core/phase_coordinator.py +257 -218
  40. crackerjack/core/resource_manager.py +14 -90
  41. crackerjack/core/service_watchdog.py +62 -95
  42. crackerjack/core/session_coordinator.py +149 -0
  43. crackerjack/core/timeout_manager.py +14 -72
  44. crackerjack/core/websocket_lifecycle.py +13 -78
  45. crackerjack/core/workflow_orchestrator.py +558 -240
  46. crackerjack/docs/INDEX.md +11 -0
  47. crackerjack/docs/generated/api/API_REFERENCE.md +10895 -0
  48. crackerjack/docs/generated/api/CLI_REFERENCE.md +109 -0
  49. crackerjack/docs/generated/api/CROSS_REFERENCES.md +1755 -0
  50. crackerjack/docs/generated/api/PROTOCOLS.md +3 -0
  51. crackerjack/docs/generated/api/SERVICES.md +1252 -0
  52. crackerjack/documentation/__init__.py +31 -0
  53. crackerjack/documentation/ai_templates.py +756 -0
  54. crackerjack/documentation/dual_output_generator.py +765 -0
  55. crackerjack/documentation/mkdocs_integration.py +518 -0
  56. crackerjack/documentation/reference_generator.py +977 -0
  57. crackerjack/dynamic_config.py +55 -50
  58. crackerjack/executors/async_hook_executor.py +10 -15
  59. crackerjack/executors/cached_hook_executor.py +117 -43
  60. crackerjack/executors/hook_executor.py +8 -34
  61. crackerjack/executors/hook_lock_manager.py +26 -183
  62. crackerjack/executors/individual_hook_executor.py +13 -11
  63. crackerjack/executors/lsp_aware_hook_executor.py +270 -0
  64. crackerjack/executors/tool_proxy.py +417 -0
  65. crackerjack/hooks/lsp_hook.py +79 -0
  66. crackerjack/intelligence/adaptive_learning.py +25 -10
  67. crackerjack/intelligence/agent_orchestrator.py +2 -5
  68. crackerjack/intelligence/agent_registry.py +34 -24
  69. crackerjack/intelligence/agent_selector.py +5 -7
  70. crackerjack/interactive.py +17 -6
  71. crackerjack/managers/async_hook_manager.py +0 -1
  72. crackerjack/managers/hook_manager.py +79 -1
  73. crackerjack/managers/publish_manager.py +66 -13
  74. crackerjack/managers/test_command_builder.py +5 -17
  75. crackerjack/managers/test_executor.py +1 -3
  76. crackerjack/managers/test_manager.py +109 -7
  77. crackerjack/managers/test_manager_backup.py +10 -9
  78. crackerjack/mcp/cache.py +2 -2
  79. crackerjack/mcp/client_runner.py +1 -1
  80. crackerjack/mcp/context.py +191 -68
  81. crackerjack/mcp/dashboard.py +7 -5
  82. crackerjack/mcp/enhanced_progress_monitor.py +31 -28
  83. crackerjack/mcp/file_monitor.py +30 -23
  84. crackerjack/mcp/progress_components.py +31 -21
  85. crackerjack/mcp/progress_monitor.py +50 -53
  86. crackerjack/mcp/rate_limiter.py +6 -6
  87. crackerjack/mcp/server_core.py +161 -32
  88. crackerjack/mcp/service_watchdog.py +2 -1
  89. crackerjack/mcp/state.py +4 -7
  90. crackerjack/mcp/task_manager.py +11 -9
  91. crackerjack/mcp/tools/core_tools.py +174 -33
  92. crackerjack/mcp/tools/error_analyzer.py +3 -2
  93. crackerjack/mcp/tools/execution_tools.py +15 -12
  94. crackerjack/mcp/tools/execution_tools_backup.py +42 -30
  95. crackerjack/mcp/tools/intelligence_tool_registry.py +7 -5
  96. crackerjack/mcp/tools/intelligence_tools.py +5 -2
  97. crackerjack/mcp/tools/monitoring_tools.py +33 -70
  98. crackerjack/mcp/tools/proactive_tools.py +24 -11
  99. crackerjack/mcp/tools/progress_tools.py +5 -8
  100. crackerjack/mcp/tools/utility_tools.py +20 -14
  101. crackerjack/mcp/tools/workflow_executor.py +62 -40
  102. crackerjack/mcp/websocket/app.py +8 -0
  103. crackerjack/mcp/websocket/endpoints.py +352 -357
  104. crackerjack/mcp/websocket/jobs.py +40 -57
  105. crackerjack/mcp/websocket/monitoring_endpoints.py +2935 -0
  106. crackerjack/mcp/websocket/server.py +7 -25
  107. crackerjack/mcp/websocket/websocket_handler.py +6 -17
  108. crackerjack/mixins/__init__.py +3 -0
  109. crackerjack/mixins/error_handling.py +145 -0
  110. crackerjack/models/config.py +21 -1
  111. crackerjack/models/config_adapter.py +49 -1
  112. crackerjack/models/protocols.py +176 -107
  113. crackerjack/models/resource_protocols.py +55 -210
  114. crackerjack/models/task.py +3 -0
  115. crackerjack/monitoring/ai_agent_watchdog.py +13 -13
  116. crackerjack/monitoring/metrics_collector.py +426 -0
  117. crackerjack/monitoring/regression_prevention.py +8 -8
  118. crackerjack/monitoring/websocket_server.py +643 -0
  119. crackerjack/orchestration/advanced_orchestrator.py +11 -6
  120. crackerjack/orchestration/coverage_improvement.py +3 -3
  121. crackerjack/orchestration/execution_strategies.py +26 -6
  122. crackerjack/orchestration/test_progress_streamer.py +8 -5
  123. crackerjack/plugins/base.py +2 -2
  124. crackerjack/plugins/hooks.py +7 -0
  125. crackerjack/plugins/managers.py +11 -8
  126. crackerjack/security/__init__.py +0 -1
  127. crackerjack/security/audit.py +90 -105
  128. crackerjack/services/anomaly_detector.py +392 -0
  129. crackerjack/services/api_extractor.py +615 -0
  130. crackerjack/services/backup_service.py +2 -2
  131. crackerjack/services/bounded_status_operations.py +15 -152
  132. crackerjack/services/cache.py +127 -1
  133. crackerjack/services/changelog_automation.py +395 -0
  134. crackerjack/services/config.py +18 -11
  135. crackerjack/services/config_merge.py +30 -85
  136. crackerjack/services/config_template.py +506 -0
  137. crackerjack/services/contextual_ai_assistant.py +48 -22
  138. crackerjack/services/coverage_badge_service.py +171 -0
  139. crackerjack/services/coverage_ratchet.py +41 -17
  140. crackerjack/services/debug.py +3 -3
  141. crackerjack/services/dependency_analyzer.py +460 -0
  142. crackerjack/services/dependency_monitor.py +14 -11
  143. crackerjack/services/documentation_generator.py +491 -0
  144. crackerjack/services/documentation_service.py +675 -0
  145. crackerjack/services/enhanced_filesystem.py +6 -5
  146. crackerjack/services/enterprise_optimizer.py +865 -0
  147. crackerjack/services/error_pattern_analyzer.py +676 -0
  148. crackerjack/services/file_hasher.py +1 -1
  149. crackerjack/services/git.py +41 -45
  150. crackerjack/services/health_metrics.py +10 -8
  151. crackerjack/services/heatmap_generator.py +735 -0
  152. crackerjack/services/initialization.py +30 -33
  153. crackerjack/services/input_validator.py +5 -97
  154. crackerjack/services/intelligent_commit.py +327 -0
  155. crackerjack/services/log_manager.py +15 -12
  156. crackerjack/services/logging.py +4 -3
  157. crackerjack/services/lsp_client.py +628 -0
  158. crackerjack/services/memory_optimizer.py +409 -0
  159. crackerjack/services/metrics.py +42 -33
  160. crackerjack/services/parallel_executor.py +416 -0
  161. crackerjack/services/pattern_cache.py +1 -1
  162. crackerjack/services/pattern_detector.py +6 -6
  163. crackerjack/services/performance_benchmarks.py +250 -576
  164. crackerjack/services/performance_cache.py +382 -0
  165. crackerjack/services/performance_monitor.py +565 -0
  166. crackerjack/services/predictive_analytics.py +510 -0
  167. crackerjack/services/quality_baseline.py +234 -0
  168. crackerjack/services/quality_baseline_enhanced.py +646 -0
  169. crackerjack/services/quality_intelligence.py +785 -0
  170. crackerjack/services/regex_patterns.py +605 -524
  171. crackerjack/services/regex_utils.py +43 -123
  172. crackerjack/services/secure_path_utils.py +5 -164
  173. crackerjack/services/secure_status_formatter.py +30 -141
  174. crackerjack/services/secure_subprocess.py +11 -92
  175. crackerjack/services/security.py +61 -30
  176. crackerjack/services/security_logger.py +18 -22
  177. crackerjack/services/server_manager.py +124 -16
  178. crackerjack/services/status_authentication.py +16 -159
  179. crackerjack/services/status_security_manager.py +4 -131
  180. crackerjack/services/terminal_utils.py +0 -0
  181. crackerjack/services/thread_safe_status_collector.py +19 -125
  182. crackerjack/services/unified_config.py +21 -13
  183. crackerjack/services/validation_rate_limiter.py +5 -54
  184. crackerjack/services/version_analyzer.py +459 -0
  185. crackerjack/services/version_checker.py +1 -1
  186. crackerjack/services/websocket_resource_limiter.py +10 -144
  187. crackerjack/services/zuban_lsp_service.py +390 -0
  188. crackerjack/slash_commands/__init__.py +2 -7
  189. crackerjack/slash_commands/run.md +2 -2
  190. crackerjack/tools/validate_input_validator_patterns.py +14 -40
  191. crackerjack/tools/validate_regex_patterns.py +19 -48
  192. {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/METADATA +197 -26
  193. crackerjack-0.33.1.dist-info/RECORD +229 -0
  194. crackerjack/CLAUDE.md +0 -207
  195. crackerjack/RULES.md +0 -380
  196. crackerjack/py313.py +0 -234
  197. crackerjack-0.32.0.dist-info/RECORD +0 -180
  198. {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/WHEEL +0 -0
  199. {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/entry_points.txt +0 -0
  200. {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,16 +1,3 @@
1
- """Global hook lock management to prevent concurrent execution of specific hooks.
2
-
3
- This module provides a lock manager that ensures certain hooks
4
- (like complexipy) run sequentially rather than concurrently to prevent
5
- resource contention and hanging processes.
6
-
7
- This implements the HookLockManagerProtocol for dependency injection compatibility.
8
-
9
- Phase 2 implementation provides enhanced file-based global lock coordination
10
- across multiple crackerjack sessions with atomic operations, heartbeat monitoring,
11
- and comprehensive stale lock cleanup.
12
- """
13
-
14
1
  import asyncio
15
2
  import json
16
3
  import logging
@@ -18,19 +5,13 @@ import os
18
5
  import time
19
6
  import typing as t
20
7
  from collections import defaultdict
21
- from contextlib import asynccontextmanager, suppress
8
+ from contextlib import AbstractAsyncContextManager, asynccontextmanager, suppress
22
9
  from pathlib import Path
23
10
 
24
11
  from ..config.global_lock_config import GlobalLockConfig
25
12
 
26
13
 
27
14
  class HookLockManager:
28
- """Manager for hook-specific locks to prevent concurrent execution.
29
-
30
- Implements HookLockManagerProtocol for dependency injection compatibility.
31
- Provides async locking with timeout protection and comprehensive monitoring.
32
- """
33
-
34
15
  _instance: t.Optional["HookLockManager"] = None
35
16
  _initialized: bool = False
36
17
 
@@ -44,37 +25,30 @@ class HookLockManager:
44
25
  return
45
26
 
46
27
  self._hooks_requiring_locks = {
47
- "complexipy", # Prevent multiple complexipy processes
48
- # Add other hooks that should run sequentially
28
+ "complexipy",
49
29
  }
50
30
 
51
- # Per-hook locks for sequential execution
52
31
  self._hook_locks: dict[str, asyncio.Lock] = defaultdict(asyncio.Lock)
53
32
 
54
- # Global lock configuration and state
55
33
  self._global_config = GlobalLockConfig()
56
34
  self._global_lock_enabled = self._global_config.enabled
57
35
  self._active_global_locks: set[str] = set()
58
36
  self._heartbeat_tasks: dict[str, asyncio.Task[None]] = {}
59
37
 
60
- # Lock usage tracking for monitoring
61
38
  self._lock_usage: dict[str, list[float]] = defaultdict(list)
62
39
  self._lock_wait_times: dict[str, list[float]] = defaultdict(list)
63
40
  self._lock_execution_times: dict[str, list[float]] = defaultdict(list)
64
- self._max_history = 50 # Keep last 50 lock acquisitions
41
+ self._max_history = 50
65
42
 
66
- # Global lock statistics tracking
67
43
  self._global_lock_attempts: dict[str, int] = defaultdict(int)
68
44
  self._global_lock_successes: dict[str, int] = defaultdict(int)
69
45
  self._global_lock_failures: dict[str, int] = defaultdict(int)
70
46
  self._stale_locks_cleaned: dict[str, int] = defaultdict(int)
71
47
  self._heartbeat_failures: dict[str, int] = defaultdict(int)
72
48
 
73
- # Timeout protection
74
- self._default_lock_timeout = 300.0 # 5 minutes default timeout
49
+ self._default_lock_timeout = 300.0
75
50
  self._lock_timeouts: dict[str, float] = {}
76
51
 
77
- # Error tracking
78
52
  self._lock_failures: dict[str, int] = defaultdict(int)
79
53
  self._timeout_failures: dict[str, int] = defaultdict(int)
80
54
 
@@ -82,41 +56,22 @@ class HookLockManager:
82
56
  self._initialized = True
83
57
 
84
58
  def requires_lock(self, hook_name: str) -> bool:
85
- """Check if a hook requires sequential execution."""
86
59
  return hook_name in self._hooks_requiring_locks
87
60
 
88
61
  @asynccontextmanager
89
- async def acquire_hook_lock(self, hook_name: str) -> t.AsyncIterator[None]:
90
- """Unified lock acquisition handling both hook-specific and global locks.
91
-
92
- Args:
93
- hook_name: Name of the hook to lock
94
-
95
- Yields:
96
- None when lock is acquired (or hook doesn't require lock)
97
-
98
- Raises:
99
- asyncio.TimeoutError: If lock acquisition times out
100
-
101
- Example:
102
- async with lock_manager.acquire_hook_lock("complexipy"):
103
- # Only one complexipy process will run at a time
104
- result = await execute_hook(hook)
105
- """
62
+ async def acquire_hook_lock(
63
+ self, hook_name: str
64
+ ) -> AbstractAsyncContextManager[None]:
106
65
  if not self.requires_lock(hook_name):
107
- # Hook doesn't require locking, proceed immediately
108
66
  yield
109
67
  return
110
68
 
111
69
  if not self._global_lock_enabled:
112
- # Use existing hook-specific locking only (legacy behavior)
113
70
  async with self._acquire_existing_hook_lock(hook_name):
114
71
  yield
115
72
  return
116
73
 
117
- # Global locking: coordinate across all crackerjack sessions
118
74
  async with self._acquire_global_coordination_lock(hook_name):
119
- # Then acquire hook-specific lock within global coordination
120
75
  async with self._acquire_existing_hook_lock(hook_name):
121
76
  yield
122
77
 
@@ -124,7 +79,6 @@ class HookLockManager:
124
79
  async def _acquire_existing_hook_lock(
125
80
  self, hook_name: str
126
81
  ) -> t.AsyncIterator[None]:
127
- """Acquire hook-specific asyncio lock (original behavior)."""
128
82
  lock = self._hook_locks[hook_name]
129
83
  timeout = self._lock_timeouts.get(hook_name, self._default_lock_timeout)
130
84
  start_time = time.time()
@@ -134,17 +88,15 @@ class HookLockManager:
134
88
  )
135
89
 
136
90
  try:
137
- # Use asyncio.wait_for to implement timeout for lock acquisition
138
91
  await asyncio.wait_for(lock.acquire(), timeout=timeout)
139
92
 
140
93
  try:
141
94
  acquisition_time = time.time() - start_time
142
95
  self.logger.info(
143
96
  f"Hook-specific lock acquired for {hook_name} after"
144
- f" {acquisition_time:.2f}s"
97
+ f" {acquisition_time: .2f}s"
145
98
  )
146
99
 
147
- # Track lock usage for monitoring
148
100
  self._track_lock_usage(hook_name, acquisition_time)
149
101
 
150
102
  execution_start = time.time()
@@ -157,11 +109,10 @@ class HookLockManager:
157
109
  self._track_lock_execution(hook_name, execution_time, total_time)
158
110
  self.logger.debug(
159
111
  f"Hook-specific lock released for {hook_name} after"
160
- f" {total_time:.2f}s total"
112
+ f" {total_time: .2f}s total"
161
113
  )
162
114
 
163
115
  finally:
164
- # Always release the lock, even if an exception occurred
165
116
  lock.release()
166
117
 
167
118
  except TimeoutError:
@@ -169,7 +120,7 @@ class HookLockManager:
169
120
  wait_time = time.time() - start_time
170
121
  self.logger.error(
171
122
  f"Hook-specific lock acquisition timeout for {hook_name} after"
172
- f" {wait_time:.2f}s "
123
+ f" {wait_time: .2f}s "
173
124
  f"(timeout: {timeout}s, total failures: "
174
125
  f"{self._timeout_failures[hook_name]})"
175
126
  )
@@ -187,7 +138,6 @@ class HookLockManager:
187
138
  async def _acquire_global_coordination_lock(
188
139
  self, hook_name: str
189
140
  ) -> t.AsyncIterator[None]:
190
- """Acquire global file-based coordination lock across crackerjack sessions."""
191
141
  lock_path = self._global_config.get_lock_path(hook_name)
192
142
  start_time = time.time()
193
143
 
@@ -196,28 +146,24 @@ class HookLockManager:
196
146
  f"Attempting global lock acquisition for {hook_name}: {lock_path}"
197
147
  )
198
148
 
199
- # Clean up stale locks first
200
149
  await self._cleanup_stale_lock_if_needed(hook_name)
201
150
 
202
151
  try:
203
- # Atomic lock acquisition with retry logic
204
152
  await self._acquire_global_lock_file(hook_name, lock_path)
205
153
  self._global_lock_successes[hook_name] += 1
206
154
  self._active_global_locks.add(hook_name)
207
155
 
208
- # Start heartbeat to keep lock alive
209
156
  heartbeat_task = asyncio.create_task(self._maintain_heartbeat(hook_name))
210
157
  self._heartbeat_tasks[hook_name] = heartbeat_task
211
158
 
212
159
  acquisition_time = time.time() - start_time
213
160
  self.logger.info(
214
- f"Global lock acquired for {hook_name} after {acquisition_time:.2f}s"
161
+ f"Global lock acquired for {hook_name} after {acquisition_time: .2f}s"
215
162
  )
216
163
 
217
164
  try:
218
165
  yield
219
166
  finally:
220
- # Cleanup: cancel heartbeat and remove lock file
221
167
  await self._cleanup_global_lock(hook_name, heartbeat_task)
222
168
 
223
169
  except Exception as e:
@@ -226,14 +172,12 @@ class HookLockManager:
226
172
  raise
227
173
 
228
174
  def _track_lock_usage(self, hook_name: str, acquisition_time: float) -> None:
229
- """Track lock acquisition times for monitoring."""
230
175
  usage_list = self._lock_usage[hook_name]
231
176
  wait_list = self._lock_wait_times[hook_name]
232
177
 
233
178
  usage_list.append(acquisition_time)
234
179
  wait_list.append(acquisition_time)
235
180
 
236
- # Keep only recent history
237
181
  if len(usage_list) > self._max_history:
238
182
  usage_list.pop(0)
239
183
  if len(wait_list) > self._max_history:
@@ -242,35 +186,31 @@ class HookLockManager:
242
186
  def _track_lock_execution(
243
187
  self, hook_name: str, execution_time: float, total_time: float
244
188
  ) -> None:
245
- """Track lock execution times for monitoring."""
246
189
  exec_list = self._lock_execution_times[hook_name]
247
190
  exec_list.append(execution_time)
248
191
 
249
- # Keep only recent history
250
192
  if len(exec_list) > self._max_history:
251
193
  exec_list.pop(0)
252
194
 
253
195
  self.logger.debug(
254
- f"Hook {hook_name} execution: {execution_time:.2f}s "
255
- f"(total with lock: {total_time:.2f}s)"
196
+ f"Hook {hook_name} execution: {execution_time: .2f}s "
197
+ f"(total with lock: {total_time: .2f}s)"
256
198
  )
257
199
 
258
200
  async def _acquire_global_lock_file(self, hook_name: str, lock_path: Path) -> None:
259
- """Atomic acquisition of global lock file with retry logic."""
260
201
  for attempt in range(self._global_config.max_retry_attempts):
261
202
  try:
262
203
  await self._attempt_lock_acquisition(hook_name, lock_path)
263
204
  return
264
205
  except FileExistsError:
265
206
  if attempt < self._global_config.max_retry_attempts - 1:
266
- # Exponential backoff with jitter
267
207
  delay = self._global_config.retry_delay_seconds * (2**attempt)
268
- jitter = delay * 0.1 # Add 10% jitter
208
+ jitter = delay * 0.1
269
209
  wait_time = delay + (jitter * (0.5 - os.urandom(1)[0] / 255))
270
210
 
271
211
  self.logger.debug(
272
212
  f"Global lock exists for {hook_name}, retrying in "
273
- f"{wait_time:.2f}s"
213
+ f"{wait_time: .2f}s"
274
214
  )
275
215
  await asyncio.sleep(wait_time)
276
216
  else:
@@ -280,7 +220,6 @@ class HookLockManager:
280
220
  )
281
221
 
282
222
  async def _attempt_lock_acquisition(self, hook_name: str, lock_path: Path) -> None:
283
- """Single atomic lock acquisition attempt using temp file + rename pattern."""
284
223
  temp_path = lock_path.with_suffix(".tmp")
285
224
 
286
225
  lock_data = {
@@ -290,39 +229,32 @@ class HookLockManager:
290
229
  "hook_name": hook_name,
291
230
  "acquired_at": time.time(),
292
231
  "last_heartbeat": time.time(),
293
- "crackerjack_version": "0.30.3", # Could be made configurable
232
+ "crackerjack_version": "0.30.3",
294
233
  }
295
234
 
296
235
  try:
297
- # Use exclusive creation for atomic operation
298
236
  with temp_path.open("x", encoding="utf-8") as f:
299
237
  json.dump(lock_data, f, indent=2)
300
238
 
301
- # Set restrictive permissions (owner only)
302
239
  temp_path.chmod(0o600)
303
240
 
304
- # Atomic rename - this is the critical section
305
241
  try:
306
242
  temp_path.rename(lock_path)
307
243
  self.logger.debug(f"Successfully created global lock file: {lock_path}")
308
244
  except FileExistsError:
309
- # Another process won the race, clean up our temp file
310
245
  with suppress(OSError):
311
246
  temp_path.unlink()
312
247
  raise
313
248
 
314
249
  except FileExistsError:
315
- # Lock file already exists - convert to proper exception type
316
250
  raise FileExistsError(f"Global lock already exists for {hook_name}")
317
251
  except Exception as e:
318
- # Clean up temp file on any error
319
252
  with suppress(OSError):
320
253
  temp_path.unlink()
321
254
  self.logger.error(f"Failed to create global lock for {hook_name}: {e}")
322
255
  raise
323
256
 
324
257
  async def _maintain_heartbeat(self, hook_name: str) -> None:
325
- """Maintain heartbeat updates to prevent stale lock detection."""
326
258
  lock_path = self._global_config.get_lock_path(hook_name)
327
259
  interval = self._global_config.session_heartbeat_interval
328
260
 
@@ -335,7 +267,6 @@ class HookLockManager:
335
267
  if hook_name not in self._active_global_locks:
336
268
  break
337
269
 
338
- # Update heartbeat timestamp in lock file
339
270
  await self._update_heartbeat_timestamp(hook_name, lock_path)
340
271
 
341
272
  except asyncio.CancelledError:
@@ -345,10 +276,9 @@ class HookLockManager:
345
276
  self._heartbeat_failures[hook_name] += 1
346
277
  self.logger.warning(f"Heartbeat update failed for {hook_name}: {e}")
347
278
 
348
- # If too many heartbeat failures, consider the lock compromised
349
279
  if self._heartbeat_failures[hook_name] > 3:
350
280
  self.logger.error(
351
- f"Too many heartbeat failures for {hook_name},"
281
+ f"Too many heartbeat failures for {hook_name}, "
352
282
  f" stopping heartbeat"
353
283
  )
354
284
  break
@@ -356,7 +286,6 @@ class HookLockManager:
356
286
  async def _update_heartbeat_timestamp(
357
287
  self, hook_name: str, lock_path: Path
358
288
  ) -> None:
359
- """Atomic update of heartbeat timestamp in existing lock file."""
360
289
  if not lock_path.exists():
361
290
  self.logger.warning(
362
291
  f"Lock file disappeared for {hook_name}, stopping heartbeat"
@@ -367,11 +296,9 @@ class HookLockManager:
367
296
  temp_path = lock_path.with_suffix(".heartbeat_tmp")
368
297
 
369
298
  try:
370
- # Read existing lock data
371
299
  with lock_path.open(encoding="utf-8") as f:
372
300
  lock_data = json.load(f)
373
301
 
374
- # Verify we still own this lock
375
302
  if lock_data.get("session_id") != self._global_config.session_id:
376
303
  self.logger.warning(
377
304
  f"Lock ownership changed for {hook_name}, stopping heartbeat"
@@ -379,10 +306,8 @@ class HookLockManager:
379
306
  self._active_global_locks.discard(hook_name)
380
307
  return
381
308
 
382
- # Update heartbeat timestamp
383
309
  lock_data["last_heartbeat"] = time.time()
384
310
 
385
- # Write updated data atomically
386
311
  with temp_path.open("w", encoding="utf-8") as f:
387
312
  json.dump(lock_data, f, indent=2)
388
313
 
@@ -397,17 +322,13 @@ class HookLockManager:
397
322
  async def _cleanup_global_lock(
398
323
  self, hook_name: str, heartbeat_task: asyncio.Task[None] | None = None
399
324
  ) -> None:
400
- """Clean up global lock resources and remove lock file."""
401
325
  self.logger.debug(f"Cleaning up global lock for {hook_name}")
402
326
 
403
- # Stop tracking this lock
404
327
  self._active_global_locks.discard(hook_name)
405
328
 
406
- # Cancel heartbeat task
407
329
  if heartbeat_task is None:
408
330
  heartbeat_task = self._heartbeat_tasks.pop(hook_name, None)
409
331
  else:
410
- # Remove from task tracking
411
332
  self._heartbeat_tasks.pop(hook_name, None)
412
333
 
413
334
  if heartbeat_task:
@@ -415,11 +336,9 @@ class HookLockManager:
415
336
  with suppress(asyncio.CancelledError):
416
337
  await heartbeat_task
417
338
 
418
- # Remove lock file
419
339
  lock_path = self._global_config.get_lock_path(hook_name)
420
340
  with suppress(OSError):
421
341
  if lock_path.exists():
422
- # Verify we still own the lock before deleting
423
342
  try:
424
343
  with lock_path.open(encoding="utf-8") as f:
425
344
  lock_data = json.load(f)
@@ -438,14 +357,12 @@ class HookLockManager:
438
357
  )
439
358
 
440
359
  async def _cleanup_stale_lock_if_needed(self, hook_name: str) -> None:
441
- """Check for and remove stale lock if detected."""
442
360
  lock_path = self._global_config.get_lock_path(hook_name)
443
361
 
444
362
  if not lock_path.exists():
445
363
  return
446
364
 
447
365
  try:
448
- # Check if lock is stale
449
366
  with lock_path.open(encoding="utf-8") as f:
450
367
  lock_data = json.load(f)
451
368
 
@@ -456,12 +373,11 @@ class HookLockManager:
456
373
 
457
374
  if age_hours > self._global_config.stale_lock_hours:
458
375
  self.logger.warning(
459
- f"Removing stale lock for {hook_name} (age: {age_hours:.2f}h)"
376
+ f"Removing stale lock for {hook_name} (age: {age_hours: .2f}h)"
460
377
  )
461
378
  lock_path.unlink()
462
379
  self._stale_locks_cleaned[hook_name] += 1
463
380
  else:
464
- # Lock is not stale, someone else has it
465
381
  owner = lock_data.get("session_id", "unknown")
466
382
  self.logger.debug(
467
383
  f"Active lock exists for {hook_name} owned by {owner}"
@@ -469,13 +385,12 @@ class HookLockManager:
469
385
 
470
386
  except Exception as e:
471
387
  self.logger.warning(f"Could not check lock staleness for {hook_name}: {e}")
472
- # If we can't read the lock file, it might be corrupted - remove it
388
+
473
389
  with suppress(OSError):
474
390
  lock_path.unlink()
475
391
  self._stale_locks_cleaned[hook_name] += 1
476
392
 
477
393
  def get_lock_stats(self) -> dict[str, t.Any]:
478
- """Get comprehensive statistics about lock usage for monitoring."""
479
394
  stats = {}
480
395
 
481
396
  for hook_name in self._hooks_requiring_locks:
@@ -518,7 +433,6 @@ class HookLockManager:
518
433
  ),
519
434
  }
520
435
 
521
- # Wait time statistics
522
436
  if wait_times:
523
437
  base_stats.update(
524
438
  {
@@ -536,7 +450,6 @@ class HookLockManager:
536
450
  }
537
451
  )
538
452
 
539
- # Execution time statistics
540
453
  if exec_times:
541
454
  base_stats.update(
542
455
  {
@@ -559,12 +472,10 @@ class HookLockManager:
559
472
  return stats
560
473
 
561
474
  def add_hook_to_lock_list(self, hook_name: str) -> None:
562
- """Add a hook to the list requiring sequential execution."""
563
475
  self._hooks_requiring_locks.add(hook_name)
564
476
  self.logger.info(f"Added {hook_name} to hooks requiring locks")
565
477
 
566
478
  def remove_hook_from_lock_list(self, hook_name: str) -> None:
567
- """Remove a hook from the list requiring sequential execution."""
568
479
  self._hooks_requiring_locks.discard(hook_name)
569
480
  if hook_name in self._hook_locks:
570
481
  del self._hook_locks[hook_name]
@@ -573,40 +484,18 @@ class HookLockManager:
573
484
  self.logger.info(f"Removed {hook_name} from hooks requiring locks")
574
485
 
575
486
  def is_hook_currently_locked(self, hook_name: str) -> bool:
576
- """Check if a hook is currently locked."""
577
487
  if not self.requires_lock(hook_name):
578
488
  return False
579
489
  return self._hook_locks[hook_name].locked()
580
490
 
581
491
  def set_hook_timeout(self, hook_name: str, timeout: float) -> None:
582
- """Set custom timeout for a specific hook.
583
-
584
- Args:
585
- hook_name: Name of the hook
586
- timeout: Timeout in seconds
587
- """
588
492
  self._lock_timeouts[hook_name] = timeout
589
493
  self.logger.info(f"Set custom timeout for {hook_name}: {timeout}s")
590
494
 
591
495
  def get_hook_timeout(self, hook_name: str) -> float:
592
- """Get timeout for a specific hook.
593
-
594
- Args:
595
- hook_name: Name of the hook
596
-
597
- Returns:
598
- Timeout in seconds
599
- """
600
496
  return self._lock_timeouts.get(hook_name, self._default_lock_timeout)
601
497
 
602
- # New protocol methods for global lock functionality
603
-
604
498
  def enable_global_lock(self, enabled: bool = True) -> None:
605
- """Enable or disable global lock functionality.
606
-
607
- Args:
608
- enabled: Whether to enable global locking
609
- """
610
499
  self._global_lock_enabled = enabled
611
500
  self._global_config.enabled = enabled
612
501
  self.logger.info(
@@ -614,33 +503,12 @@ class HookLockManager:
614
503
  )
615
504
 
616
505
  def is_global_lock_enabled(self) -> bool:
617
- """Check if global lock functionality is enabled.
618
-
619
- Returns:
620
- True if global locking is enabled
621
- """
622
506
  return self._global_lock_enabled
623
507
 
624
508
  def get_global_lock_path(self, hook_name: str) -> Path:
625
- """Get the filesystem path for a hook's global lock file.
626
-
627
- Args:
628
- hook_name: Name of the hook
629
-
630
- Returns:
631
- Path to the lock file for the hook
632
- """
633
509
  return self._global_config.get_lock_path(hook_name)
634
510
 
635
511
  def cleanup_stale_locks(self, max_age_hours: float = 2.0) -> int:
636
- """Clean up stale lock files older than max_age_hours.
637
-
638
- Args:
639
- max_age_hours: Maximum age in hours before a lock is considered stale
640
-
641
- Returns:
642
- Number of stale locks cleaned up
643
- """
644
512
  locks_dir = self._global_config.lock_directory
645
513
  if not locks_dir.exists():
646
514
  return 0
@@ -665,9 +533,7 @@ class HookLockManager:
665
533
  def _process_lock_file(
666
534
  self, lock_file: Path, max_age_hours: float, current_time: float
667
535
  ) -> int:
668
- """Process a single lock file and return number of files cleaned."""
669
536
  try:
670
- # Check file age
671
537
  file_age_hours = (current_time - lock_file.stat().st_mtime) / 3600
672
538
 
673
539
  if file_age_hours > max_age_hours:
@@ -683,7 +549,6 @@ class HookLockManager:
683
549
  def _cleanup_stale_lock_file(
684
550
  self, lock_file: Path, max_age_hours: float, current_time: float
685
551
  ) -> int:
686
- """Clean up a stale lock file and return 1 if successful."""
687
552
  try:
688
553
  with lock_file.open(encoding="utf-8") as f:
689
554
  lock_data = json.load(f)
@@ -698,12 +563,11 @@ class HookLockManager:
698
563
  hook_name = lock_file.stem
699
564
  self._stale_locks_cleaned[hook_name] += 1
700
565
  self.logger.info(
701
- f"Cleaned stale lock: {lock_file} (age: {heartbeat_age_hours:.2f}h)"
566
+ f"Cleaned stale lock: {lock_file} (age: {heartbeat_age_hours: .2f}h)"
702
567
  )
703
568
  return 1
704
569
 
705
570
  except (json.JSONDecodeError, KeyError):
706
- # Corrupted lock file, remove it
707
571
  lock_file.unlink()
708
572
  self.logger.warning(f"Cleaned corrupted lock file: {lock_file}")
709
573
  return 1
@@ -711,17 +575,12 @@ class HookLockManager:
711
575
  return 0
712
576
 
713
577
  def get_global_lock_stats(self) -> dict[str, t.Any]:
714
- """Get comprehensive statistics about global lock usage.
715
-
716
- Returns:
717
- Dictionary containing global lock statistics and metrics
718
- """
719
578
  stats: dict[str, t.Any] = {
720
579
  "global_lock_enabled": self._global_lock_enabled,
721
580
  "lock_directory": str(self._global_config.lock_directory),
722
581
  "session_id": self._global_config.session_id,
723
582
  "hostname": self._global_config.hostname,
724
- "active_global_locks": list(self._active_global_locks),
583
+ "active_global_locks": list[t.Any](self._active_global_locks),
725
584
  "active_heartbeat_tasks": len(self._heartbeat_tasks),
726
585
  "configuration": {
727
586
  "timeout_seconds": self._global_config.timeout_seconds,
@@ -734,11 +593,10 @@ class HookLockManager:
734
593
  "statistics": {},
735
594
  }
736
595
 
737
- # Per-hook global lock statistics
738
596
  all_hooks = (
739
- set(self._global_lock_attempts.keys())
740
- | set(self._global_lock_successes.keys())
741
- | set(self._global_lock_failures.keys())
597
+ set[t.Any](self._global_lock_attempts.keys())
598
+ | set[t.Any](self._global_lock_successes.keys())
599
+ | set[t.Any](self._global_lock_failures.keys())
742
600
  )
743
601
 
744
602
  for hook_name in all_hooks:
@@ -761,7 +619,6 @@ class HookLockManager:
761
619
  "has_heartbeat_task": hook_name in self._heartbeat_tasks,
762
620
  }
763
621
 
764
- # Overall statistics
765
622
  total_attempts = sum(self._global_lock_attempts.values())
766
623
  total_successes = sum(self._global_lock_successes.values())
767
624
  total_failures = sum(self._global_lock_failures.values())
@@ -782,32 +639,21 @@ class HookLockManager:
782
639
  return stats
783
640
 
784
641
  def configure_from_options(self, options: t.Any) -> None:
785
- """Configure the lock manager from CLI options.
786
-
787
- Args:
788
- options: Options object containing CLI arguments
789
- """
790
642
  self._global_config = GlobalLockConfig.from_options(options)
791
643
  self._global_lock_enabled = self._global_config.enabled
792
644
 
793
- # Apply stale lock cleanup if requested
794
645
  if hasattr(options, "global_lock_cleanup") and options.global_lock_cleanup:
795
646
  self.cleanup_stale_locks()
796
647
 
797
648
  self.logger.info(
798
649
  f"Configured lock manager: global_locks={
799
650
  'enabled' if self._global_lock_enabled else 'disabled'
800
- },"
651
+ }, "
801
652
  f" timeout={self._global_config.timeout_seconds}s, "
802
653
  f"lock_dir={self._global_config.lock_directory}"
803
654
  )
804
655
 
805
656
  def reset_hook_stats(self, hook_name: str | None = None) -> None:
806
- """Reset statistics for a specific hook or all hooks.
807
-
808
- Args:
809
- hook_name: Name of the hook to reset, or None for all hooks
810
- """
811
657
  if hook_name:
812
658
  self._lock_usage[hook_name].clear()
813
659
  self._lock_wait_times[hook_name].clear()
@@ -824,9 +670,8 @@ class HookLockManager:
824
670
  self.logger.info("Reset statistics for all hooks")
825
671
 
826
672
  def get_comprehensive_status(self) -> dict[str, t.Any]:
827
- """Get comprehensive status including configuration and health."""
828
673
  status = {
829
- "hooks_requiring_locks": list(self._hooks_requiring_locks),
674
+ "hooks_requiring_locks": list[t.Any](self._hooks_requiring_locks),
830
675
  "default_timeout": self._default_lock_timeout,
831
676
  "custom_timeouts": self._lock_timeouts.copy(),
832
677
  "max_history": self._max_history,
@@ -840,7 +685,6 @@ class HookLockManager:
840
685
  "total_timeout_failures": sum(self._timeout_failures.values()),
841
686
  }
842
687
 
843
- # Add global lock information if enabled
844
688
  if self._global_lock_enabled:
845
689
  status["global_lock_stats"] = self.get_global_lock_stats()
846
690
  else:
@@ -852,5 +696,4 @@ class HookLockManager:
852
696
  return status
853
697
 
854
698
 
855
- # Singleton instance
856
699
  hook_lock_manager = HookLockManager()