crackerjack 0.33.0__py3-none-any.whl → 0.33.2__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 (198) 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 +4 -13
  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 +104 -204
  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 +171 -174
  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 +44 -8
  74. crackerjack/managers/test_command_builder.py +1 -15
  75. crackerjack/managers/test_executor.py +1 -3
  76. crackerjack/managers/test_manager.py +98 -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 +17 -16
  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 +173 -32
  92. crackerjack/mcp/tools/error_analyzer.py +3 -2
  93. crackerjack/mcp/tools/execution_tools.py +8 -10
  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 +0 -2
  109. crackerjack/mixins/error_handling.py +1 -70
  110. crackerjack/models/config.py +12 -1
  111. crackerjack/models/config_adapter.py +49 -1
  112. crackerjack/models/protocols.py +122 -122
  113. crackerjack/models/resource_protocols.py +55 -210
  114. crackerjack/monitoring/ai_agent_watchdog.py +13 -13
  115. crackerjack/monitoring/metrics_collector.py +426 -0
  116. crackerjack/monitoring/regression_prevention.py +8 -8
  117. crackerjack/monitoring/websocket_server.py +643 -0
  118. crackerjack/orchestration/advanced_orchestrator.py +11 -6
  119. crackerjack/orchestration/coverage_improvement.py +3 -3
  120. crackerjack/orchestration/execution_strategies.py +26 -6
  121. crackerjack/orchestration/test_progress_streamer.py +8 -5
  122. crackerjack/plugins/base.py +2 -2
  123. crackerjack/plugins/hooks.py +7 -0
  124. crackerjack/plugins/managers.py +11 -8
  125. crackerjack/security/__init__.py +0 -1
  126. crackerjack/security/audit.py +6 -35
  127. crackerjack/services/anomaly_detector.py +392 -0
  128. crackerjack/services/api_extractor.py +615 -0
  129. crackerjack/services/backup_service.py +2 -2
  130. crackerjack/services/bounded_status_operations.py +15 -152
  131. crackerjack/services/cache.py +127 -1
  132. crackerjack/services/changelog_automation.py +395 -0
  133. crackerjack/services/config.py +15 -9
  134. crackerjack/services/config_merge.py +19 -80
  135. crackerjack/services/config_template.py +506 -0
  136. crackerjack/services/contextual_ai_assistant.py +48 -22
  137. crackerjack/services/coverage_badge_service.py +171 -0
  138. crackerjack/services/coverage_ratchet.py +27 -25
  139. crackerjack/services/debug.py +3 -3
  140. crackerjack/services/dependency_analyzer.py +460 -0
  141. crackerjack/services/dependency_monitor.py +14 -11
  142. crackerjack/services/documentation_generator.py +491 -0
  143. crackerjack/services/documentation_service.py +675 -0
  144. crackerjack/services/enhanced_filesystem.py +6 -5
  145. crackerjack/services/enterprise_optimizer.py +865 -0
  146. crackerjack/services/error_pattern_analyzer.py +676 -0
  147. crackerjack/services/file_hasher.py +1 -1
  148. crackerjack/services/git.py +8 -25
  149. crackerjack/services/health_metrics.py +10 -8
  150. crackerjack/services/heatmap_generator.py +735 -0
  151. crackerjack/services/initialization.py +11 -30
  152. crackerjack/services/input_validator.py +5 -97
  153. crackerjack/services/intelligent_commit.py +327 -0
  154. crackerjack/services/log_manager.py +15 -12
  155. crackerjack/services/logging.py +4 -3
  156. crackerjack/services/lsp_client.py +628 -0
  157. crackerjack/services/memory_optimizer.py +19 -87
  158. crackerjack/services/metrics.py +42 -33
  159. crackerjack/services/parallel_executor.py +9 -67
  160. crackerjack/services/pattern_cache.py +1 -1
  161. crackerjack/services/pattern_detector.py +6 -6
  162. crackerjack/services/performance_benchmarks.py +18 -59
  163. crackerjack/services/performance_cache.py +20 -81
  164. crackerjack/services/performance_monitor.py +27 -95
  165. crackerjack/services/predictive_analytics.py +510 -0
  166. crackerjack/services/quality_baseline.py +234 -0
  167. crackerjack/services/quality_baseline_enhanced.py +646 -0
  168. crackerjack/services/quality_intelligence.py +785 -0
  169. crackerjack/services/regex_patterns.py +618 -524
  170. crackerjack/services/regex_utils.py +43 -123
  171. crackerjack/services/secure_path_utils.py +5 -164
  172. crackerjack/services/secure_status_formatter.py +30 -141
  173. crackerjack/services/secure_subprocess.py +11 -92
  174. crackerjack/services/security.py +9 -41
  175. crackerjack/services/security_logger.py +12 -24
  176. crackerjack/services/server_manager.py +124 -16
  177. crackerjack/services/status_authentication.py +16 -159
  178. crackerjack/services/status_security_manager.py +4 -131
  179. crackerjack/services/thread_safe_status_collector.py +19 -125
  180. crackerjack/services/unified_config.py +21 -13
  181. crackerjack/services/validation_rate_limiter.py +5 -54
  182. crackerjack/services/version_analyzer.py +459 -0
  183. crackerjack/services/version_checker.py +1 -1
  184. crackerjack/services/websocket_resource_limiter.py +10 -144
  185. crackerjack/services/zuban_lsp_service.py +390 -0
  186. crackerjack/slash_commands/__init__.py +2 -7
  187. crackerjack/slash_commands/run.md +2 -2
  188. crackerjack/tools/validate_input_validator_patterns.py +14 -40
  189. crackerjack/tools/validate_regex_patterns.py +19 -48
  190. {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/METADATA +196 -25
  191. crackerjack-0.33.2.dist-info/RECORD +229 -0
  192. crackerjack/CLAUDE.md +0 -207
  193. crackerjack/RULES.md +0 -380
  194. crackerjack/py313.py +0 -234
  195. crackerjack-0.33.0.dist-info/RECORD +0 -187
  196. {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/WHEEL +0 -0
  197. {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/entry_points.txt +0 -0
  198. {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/licenses/LICENSE +0 -0
@@ -1,10 +1,3 @@
1
- """
2
- Service watchdog with timeout protection and automatic recovery.
3
-
4
- This module provides comprehensive monitoring of crackerjack services
5
- with automatic restart capabilities and hanging prevention.
6
- """
7
-
8
1
  import asyncio
9
2
  import contextlib
10
3
  import logging
@@ -24,8 +17,6 @@ logger = logging.getLogger("crackerjack.service_watchdog")
24
17
 
25
18
 
26
19
  class ServiceState(Enum):
27
- """Service states."""
28
-
29
20
  STOPPED = "stopped"
30
21
  STARTING = "starting"
31
22
  RUNNING = "running"
@@ -36,8 +27,6 @@ class ServiceState(Enum):
36
27
 
37
28
  @dataclass
38
29
  class ServiceConfig:
39
- """Configuration for a monitored service."""
40
-
41
30
  name: str
42
31
  command: list[str]
43
32
  health_check_url: str | None = None
@@ -52,8 +41,6 @@ class ServiceConfig:
52
41
 
53
42
  @dataclass
54
43
  class ServiceStatus:
55
- """Status of a monitored service."""
56
-
57
44
  config: ServiceConfig
58
45
  state: ServiceState = ServiceState.STOPPED
59
46
  process: subprocess.Popen[bytes] | None = None
@@ -66,14 +53,12 @@ class ServiceStatus:
66
53
 
67
54
  @property
68
55
  def uptime(self) -> float:
69
- """Get service uptime in seconds."""
70
56
  if self.state == ServiceState.RUNNING and self.last_start_time > 0:
71
57
  return time.time() - self.last_start_time
72
58
  return 0.0
73
59
 
74
60
  @property
75
61
  def is_healthy(self) -> bool:
76
- """Check if service is healthy."""
77
62
  return (
78
63
  self.state == ServiceState.RUNNING
79
64
  and self.process is not None
@@ -83,8 +68,6 @@ class ServiceStatus:
83
68
 
84
69
 
85
70
  class ServiceWatchdog:
86
- """Watchdog for monitoring and managing services with timeout protection."""
87
-
88
71
  def __init__(self, console: Console | None = None) -> None:
89
72
  self.console = console or Console()
90
73
  self.timeout_manager = get_timeout_manager()
@@ -92,7 +75,6 @@ class ServiceWatchdog:
92
75
  self.is_running = False
93
76
  self.monitor_task: asyncio.Task[None] | None = None
94
77
 
95
- # Default service configurations
96
78
  self.default_configs = {
97
79
  "mcp_server": ServiceConfig(
98
80
  name="MCP Server",
@@ -103,53 +85,55 @@ class ServiceWatchdog:
103
85
  "websocket_server": ServiceConfig(
104
86
  name="WebSocket Server",
105
87
  command=["python", "-m", "crackerjack", "--start-websocket-server"],
106
- health_check_url="http://localhost:8675/",
88
+ health_check_url="http: //localhost: 8675/",
107
89
  health_check_timeout=3.0,
108
90
  startup_timeout=20.0,
109
91
  shutdown_timeout=10.0,
110
92
  ),
93
+ "zuban_lsp": ServiceConfig(
94
+ name="Zuban LSP Server",
95
+ command=["uv", "run", "zuban", "server"],
96
+ startup_timeout=15.0,
97
+ shutdown_timeout=10.0,
98
+ max_restarts=5,
99
+ restart_delay=5.0,
100
+ restart_backoff_multiplier=2.0,
101
+ max_restart_delay=300.0,
102
+ ),
111
103
  }
112
104
 
113
105
  def add_service(self, service_id: str, config: ServiceConfig) -> None:
114
- """Add a service to monitor."""
115
106
  self.services[service_id] = ServiceStatus(config=config)
116
107
  logger.info(f"Added service {service_id} to watchdog")
117
108
 
118
109
  def remove_service(self, service_id: str) -> None:
119
- """Remove a service from monitoring."""
120
110
  if service_id in self.services:
121
111
  asyncio.create_task(self.stop_service(service_id))
122
112
  del self.services[service_id]
123
113
  logger.info(f"Removed service {service_id} from watchdog")
124
114
 
125
115
  async def start_watchdog(self) -> None:
126
- """Start the watchdog monitoring."""
127
116
  if self.is_running:
128
117
  return
129
118
 
130
119
  self.is_running = True
131
120
 
132
- # Add default services
133
121
  for service_id, config in self.default_configs.items():
134
122
  self.add_service(service_id, config)
135
123
 
136
- # Start monitoring task with timeout protection
137
124
  self.monitor_task = asyncio.create_task(self._monitor_services())
138
125
 
139
- # Setup signal handlers for graceful shutdown
140
126
  self._setup_signal_handlers()
141
127
 
142
128
  self.console.print("[green]🐕 Service Watchdog started[/green]")
143
129
  logger.info("Service watchdog started")
144
130
 
145
131
  async def stop_watchdog(self) -> None:
146
- """Stop the watchdog and all monitored services."""
147
132
  if not self.is_running:
148
133
  return
149
134
 
150
135
  self.is_running = False
151
136
 
152
- # Cancel monitoring task
153
137
  if self.monitor_task and not self.monitor_task.done():
154
138
  self.monitor_task.cancel()
155
139
  try:
@@ -157,7 +141,6 @@ class ServiceWatchdog:
157
141
  except asyncio.CancelledError:
158
142
  pass
159
143
 
160
- # Stop all services
161
144
  stop_tasks = [
162
145
  self.stop_service(service_id) for service_id in self.services.keys()
163
146
  ]
@@ -168,7 +151,6 @@ class ServiceWatchdog:
168
151
  logger.info("Service watchdog stopped")
169
152
 
170
153
  async def start_service(self, service_id: str) -> bool:
171
- """Start a specific service with timeout protection."""
172
154
  if not self._validate_service_start_request(service_id):
173
155
  return False
174
156
 
@@ -180,7 +162,6 @@ class ServiceWatchdog:
180
162
  return self._handle_service_start_failure(service, service_id, e)
181
163
 
182
164
  def _validate_service_start_request(self, service_id: str) -> bool:
183
- """Validate if service can be started."""
184
165
  if service_id not in self.services:
185
166
  return False
186
167
 
@@ -190,7 +171,6 @@ class ServiceWatchdog:
190
171
  async def _execute_service_startup(
191
172
  self, service_id: str, service: ServiceStatus
192
173
  ) -> bool:
193
- """Execute the service startup process with timeout protection."""
194
174
  async with self.timeout_manager.timeout_context(
195
175
  f"start_service_{service_id}",
196
176
  timeout=service.config.startup_timeout,
@@ -208,13 +188,10 @@ class ServiceWatchdog:
208
188
  return True
209
189
 
210
190
  def _prepare_service_startup(self, service: ServiceStatus) -> None:
211
- """Prepare service for startup."""
212
191
  service.state = ServiceState.STARTING
213
192
  service.last_start_time = time.time()
214
193
 
215
194
  async def _start_service_process(self, service: ServiceStatus) -> bool:
216
- """Start the service process and verify it's running."""
217
- # Start the service process with security logging
218
195
  security_logger = get_security_logger()
219
196
  security_logger.log_subprocess_execution(
220
197
  command=service.config.command,
@@ -228,10 +205,8 @@ class ServiceWatchdog:
228
205
  start_new_session=True,
229
206
  )
230
207
 
231
- # Wait for process to stabilize
232
208
  await asyncio.sleep(2)
233
209
 
234
- # Check if process is still running
235
210
  if service.process.poll() is not None:
236
211
  service.state = ServiceState.FAILED
237
212
  service.last_error = "Process exited immediately"
@@ -240,7 +215,6 @@ class ServiceWatchdog:
240
215
  return True
241
216
 
242
217
  async def _verify_service_health(self, service: ServiceStatus) -> bool:
243
- """Verify service health if health check is configured."""
244
218
  if not service.config.health_check_url:
245
219
  return True
246
220
 
@@ -256,7 +230,6 @@ class ServiceWatchdog:
256
230
  def _finalize_successful_startup(
257
231
  self, service: ServiceStatus, service_id: str
258
232
  ) -> None:
259
- """Finalize successful service startup."""
260
233
  service.state = ServiceState.RUNNING
261
234
  service.consecutive_failures = 0
262
235
  service.health_check_failures = 0
@@ -267,7 +240,6 @@ class ServiceWatchdog:
267
240
  def _handle_service_start_failure(
268
241
  self, service: ServiceStatus, service_id: str, error: Exception
269
242
  ) -> bool:
270
- """Handle service startup failure."""
271
243
  service.state = ServiceState.FAILED
272
244
  service.last_error = str(error)
273
245
  service.consecutive_failures += 1
@@ -282,7 +254,6 @@ class ServiceWatchdog:
282
254
  return False
283
255
 
284
256
  async def stop_service(self, service_id: str) -> bool:
285
- """Stop a specific service with timeout protection."""
286
257
  if service_id not in self.services:
287
258
  return False
288
259
 
@@ -320,17 +291,15 @@ class ServiceWatchdog:
320
291
  return False
321
292
 
322
293
  async def _monitor_services(self) -> None:
323
- """Main monitoring loop with timeout protection."""
324
294
  while self.is_running:
325
295
  try:
326
296
  async with self.timeout_manager.timeout_context(
327
297
  "monitor_services",
328
- timeout=30.0, # Monitor cycle timeout
298
+ timeout=30.0,
329
299
  strategy=TimeoutStrategy.GRACEFUL_DEGRADATION,
330
300
  ):
331
- # Check each service
332
301
  for service_id, service in self.services.items():
333
- if not self.is_running: # Check if shutdown requested
302
+ if not self.is_running:
334
303
  break
335
304
 
336
305
  try:
@@ -338,19 +307,16 @@ class ServiceWatchdog:
338
307
  except Exception as e:
339
308
  logger.error(f"Error checking service {service_id}: {e}")
340
309
 
341
- # Wait before next check cycle
342
- await asyncio.sleep(10) # Check every 10 seconds
310
+ await asyncio.sleep(10)
343
311
 
344
312
  except Exception as e:
345
313
  logger.error(f"Monitor services error: {e}")
346
- await asyncio.sleep(30) # Longer delay on error
314
+ await asyncio.sleep(30)
347
315
 
348
316
  async def _check_service_health(
349
317
  self, service_id: str, service: ServiceStatus
350
318
  ) -> None:
351
- """Check health of a single service."""
352
319
  if service.state == ServiceState.RUNNING:
353
- # Check if process is still alive
354
320
  if service.process and service.process.poll() is not None:
355
321
  service.state = ServiceState.FAILED
356
322
  service.last_error = (
@@ -362,7 +328,6 @@ class ServiceWatchdog:
362
328
  return
363
329
 
364
330
  async def _perform_health_check(self, service: ServiceStatus) -> bool:
365
- """Perform HTTP health check with timeout protection."""
366
331
  if not service.config.health_check_url:
367
332
  return True
368
333
 
@@ -382,21 +347,17 @@ class ServiceWatchdog:
382
347
  return False
383
348
 
384
349
  async def _terminate_process(self, service: ServiceStatus) -> None:
385
- """Terminate service process gracefully with timeout."""
386
350
  if not service.process:
387
351
  return
388
352
 
389
353
  try:
390
- # Try graceful termination first
391
354
  service.process.terminate()
392
355
 
393
- # Wait for graceful shutdown
394
356
  try:
395
357
  await asyncio.wait_for(
396
358
  self._wait_for_process_exit(service.process), timeout=5.0
397
359
  )
398
360
  except TimeoutError:
399
- # Force kill if graceful shutdown fails
400
361
  service.process.kill()
401
362
  await asyncio.wait_for(
402
363
  self._wait_for_process_exit(service.process), timeout=2.0
@@ -404,21 +365,17 @@ class ServiceWatchdog:
404
365
 
405
366
  except Exception as e:
406
367
  logger.warning(f"Error terminating process: {e}")
407
- # Last resort: force kill
368
+
408
369
  with contextlib.suppress(Exception):
409
370
  service.process.kill()
410
371
 
411
372
  async def _wait_for_process_exit(self, process: subprocess.Popen[bytes]) -> None:
412
- """Wait for process to exit."""
413
373
  while process.poll() is None:
414
374
  await asyncio.sleep(0.1)
415
375
 
416
376
  def _setup_signal_handlers(self) -> None:
417
- """Setup signal handlers for graceful shutdown."""
418
-
419
- def signal_handler(signum: int, frame: object) -> None: # noqa: ARG001
420
- """Handle termination signals."""
421
- _ = frame # Signal handler frame - required by signal API
377
+ def signal_handler(signum: int, frame: object) -> None:
378
+ _ = frame
422
379
  logger.info(f"Received signal {signum}, stopping watchdog...")
423
380
  asyncio.create_task(self.stop_watchdog())
424
381
 
@@ -426,64 +383,74 @@ class ServiceWatchdog:
426
383
  signal.signal(signal.SIGTERM, signal_handler)
427
384
 
428
385
  def get_service_status(self, service_id: str) -> ServiceStatus | None:
429
- """Get status of a specific service."""
430
386
  return self.services.get(service_id)
431
387
 
432
388
  def get_all_services_status(self) -> dict[str, ServiceStatus]:
433
- """Get status of all services."""
434
389
  return self.services.copy()
435
390
 
436
391
  def print_status_report(self) -> None:
437
- """Print formatted status report."""
438
- self.console.print("\n[bold blue]🐕 Service Watchdog Status[/bold blue]")
439
- self.console.print("=" * 50)
392
+ """Print detailed status report for all services."""
393
+ self._print_report_header()
440
394
 
441
395
  if not self.services:
442
396
  self.console.print("[dim]No services configured[/dim]")
443
397
  return
444
398
 
399
+ table = self._create_status_table()
400
+ self.console.print(table)
401
+
402
+ def _print_report_header(self) -> None:
403
+ """Print the status report header."""
404
+ self.console.print("\n[bold blue]🐕 Service Watchdog Status[/bold blue]")
405
+ self.console.print("=" * 50)
406
+
407
+ def _create_status_table(self) -> Table:
408
+ """Create and populate the status table."""
445
409
  table = Table()
446
410
  table.add_column("Service")
447
411
  table.add_column("Status")
448
412
  table.add_column("Uptime")
449
413
 
450
414
  for service in self.services.values():
451
- # Status emoji and color
452
- if service.state == ServiceState.RUNNING and service.is_healthy:
453
- status = "[green]🟢 Running[/green]"
454
- elif service.state == ServiceState.STARTING:
455
- status = "[yellow]🟡 Starting[/yellow]"
456
- elif service.state == ServiceState.STOPPING:
457
- status = "[yellow]🟡 Stopping[/yellow]"
458
- elif service.state == ServiceState.FAILED:
459
- status = "[red]🔴 Failed[/red]"
460
- elif service.state == ServiceState.TIMEOUT:
461
- status = "[red] Timeout[/red]"
462
- else:
463
- status = "[dim] Stopped[/dim]"
464
-
465
- # Format uptime
466
- uptime = service.uptime
467
- if uptime > 3600:
468
- uptime_str = f"{uptime / 3600:.1f}h"
469
- elif uptime > 60:
470
- uptime_str = f"{uptime / 60:.1f}m"
471
- elif uptime > 0:
472
- uptime_str = f"{uptime:.0f}s"
473
- else:
474
- uptime_str = "-"
475
-
476
- table.add_row(service.config.name, status, uptime_str)
415
+ status_display = self._get_service_status_display(service)
416
+ uptime_display = self._format_uptime(service.uptime)
417
+ table.add_row(service.config.name, status_display, uptime_display)
418
+
419
+ return table
420
+
421
+ def _get_service_status_display(self, service: ServiceStatus) -> str:
422
+ """Get formatted status display for a service."""
423
+ status_map = {
424
+ (ServiceState.RUNNING, True): "[green]🟢 Running[/green]",
425
+ (ServiceState.STARTING, None): "[yellow]🟡 Starting[/yellow]",
426
+ (ServiceState.STOPPING, None): "[yellow]🟡 Stopping[/yellow]",
427
+ (ServiceState.FAILED, None): "[red]🔴 Failed[/red]",
428
+ (ServiceState.TIMEOUT, None): "[red]⏰ Timeout[/red]",
429
+ }
477
430
 
478
- self.console.print(table)
431
+ # Check for running with healthy status first
432
+ if service.state == ServiceState.RUNNING and service.is_healthy:
433
+ return status_map[(ServiceState.RUNNING, True)]
434
+
435
+ # Check other states
436
+ status_key = (service.state, None)
437
+ return status_map.get(status_key, "[dim]⚫ Stopped[/dim]")
438
+
439
+ def _format_uptime(self, uptime: float) -> str:
440
+ """Format uptime duration for display."""
441
+ if uptime > 3600:
442
+ return f"{uptime / 3600: .1f}h"
443
+ elif uptime > 60:
444
+ return f"{uptime / 60: .1f}m"
445
+ elif uptime > 0:
446
+ return f"{uptime: .0f}s"
447
+ return "-"
479
448
 
480
449
 
481
- # Global service watchdog instance
482
450
  _global_watchdog: ServiceWatchdog | None = None
483
451
 
484
452
 
485
453
  def get_service_watchdog(console: Console | None = None) -> ServiceWatchdog:
486
- """Get global service watchdog instance."""
487
454
  global _global_watchdog
488
455
  if _global_watchdog is None:
489
456
  _global_watchdog = ServiceWatchdog(console)
@@ -45,6 +45,10 @@ class SessionCoordinator:
45
45
  def end_session(self, success: bool = True) -> None:
46
46
  self.success = success
47
47
  self.end_time = time.time()
48
+
49
+ # Capture quality metrics at session end
50
+ self._capture_quality_metrics()
51
+
48
52
  if success:
49
53
  self.complete_task("session", "Session completed successfully")
50
54
  else:
@@ -283,3 +287,148 @@ class SessionCoordinator:
283
287
  def update_stage(self, stage: str, status: str) -> None:
284
288
  if self.web_job_id:
285
289
  self._update_websocket_progress(status, f"{stage}: {status}")
290
+
291
+ def _capture_quality_metrics(self) -> None:
292
+ """Capture quality metrics at the end of the session."""
293
+ try:
294
+ quality_service = self._initialize_quality_service()
295
+ metrics = self._extract_session_metrics()
296
+
297
+ if metrics:
298
+ self._record_quality_baseline(quality_service, metrics)
299
+ report = quality_service.generate_comprehensive_report(metrics)
300
+ self._display_quality_report(report)
301
+ except Exception as e:
302
+ self._handle_quality_tracking_error(e)
303
+
304
+ def _initialize_quality_service(self) -> t.Any:
305
+ """Initialize the quality baseline service."""
306
+ from crackerjack.services.quality_baseline_enhanced import (
307
+ EnhancedQualityBaselineService,
308
+ )
309
+
310
+ return EnhancedQualityBaselineService()
311
+
312
+ def _record_quality_baseline(
313
+ self, quality_service: t.Any, metrics: dict[str, t.Any]
314
+ ) -> None:
315
+ """Record quality baseline with metrics."""
316
+ quality_service.record_baseline(
317
+ coverage_percent=metrics.get("coverage_percent", 0.0),
318
+ test_count=metrics.get("test_count", 0),
319
+ test_pass_rate=metrics.get("test_pass_rate", 100.0),
320
+ hook_failures=metrics.get("hook_failures", 0),
321
+ complexity_violations=metrics.get("complexity_violations", 0),
322
+ security_issues=metrics.get("security_issues", 0),
323
+ type_errors=metrics.get("type_errors", 0),
324
+ linting_issues=metrics.get("linting_issues", 0),
325
+ )
326
+
327
+ def _handle_quality_tracking_error(self, error: Exception) -> None:
328
+ """Handle quality tracking errors without failing the session."""
329
+ self.console.print(
330
+ f"[dim yellow]Warning: Quality tracking failed: {error}[/dim yellow]"
331
+ )
332
+
333
+ def _extract_session_metrics(self) -> dict[str, t.Any] | None:
334
+ """Extract quality metrics from the current session."""
335
+ with suppress(Exception):
336
+ metrics: dict[str, t.Any] = {}
337
+ self._extract_test_metrics(metrics)
338
+ self._extract_hook_metrics(metrics)
339
+ self._set_default_metrics(metrics)
340
+ return metrics or None
341
+ return None
342
+
343
+ def _extract_test_metrics(self, metrics: dict[str, t.Any]) -> None:
344
+ """Extract test-related metrics from tasks."""
345
+ if "testing" not in self.tasks:
346
+ return
347
+
348
+ test_task = self.tasks["testing"]
349
+
350
+ if hasattr(test_task, "coverage_percent"):
351
+ metrics["coverage_percent"] = getattr(test_task, "coverage_percent", 0.0)
352
+ if hasattr(test_task, "test_count"):
353
+ metrics["test_count"] = getattr(test_task, "test_count", 0)
354
+ if hasattr(test_task, "test_pass_rate"):
355
+ metrics["test_pass_rate"] = getattr(test_task, "test_pass_rate", 0.0)
356
+
357
+ def _extract_hook_metrics(self, metrics: dict[str, t.Any]) -> None:
358
+ """Extract hook failure metrics from tasks."""
359
+ hook_failures = 0
360
+ for task_name, task in self.tasks.items():
361
+ if "hooks" in task_name and hasattr(task, "status"):
362
+ if getattr(task, "status") == "failed":
363
+ hook_failures += 1
364
+ metrics["hook_failures"] = hook_failures
365
+
366
+ def _set_default_metrics(self, metrics: dict[str, t.Any]) -> None:
367
+ """Set default values for metrics we don't have direct access to."""
368
+ defaults = {
369
+ "coverage_percent": 0.0,
370
+ "test_count": 0,
371
+ "test_pass_rate": 100.0 if self.success else 0.0,
372
+ "complexity_violations": 0,
373
+ "security_issues": 0,
374
+ "type_errors": 0,
375
+ "linting_issues": 0,
376
+ }
377
+ for key, default_value in defaults.items():
378
+ metrics.setdefault(key, default_value)
379
+
380
+ def _display_quality_report(self, report: t.Any) -> None:
381
+ """Display a summary of the quality report."""
382
+ with suppress(Exception):
383
+ if not report.current_metrics:
384
+ return
385
+
386
+ self._display_quality_score(report)
387
+ self._display_quality_trend(report)
388
+ self._display_critical_alerts(report)
389
+ self._display_top_recommendations(report)
390
+
391
+ def _display_quality_score(self, report: t.Any) -> None:
392
+ """Display the quality score."""
393
+ score = report.current_metrics.quality_score
394
+ self.console.print(f"\n[cyan]📊 Quality Score: {score}/100[/cyan]")
395
+
396
+ def _display_quality_trend(self, report: t.Any) -> None:
397
+ """Display quality trend information."""
398
+ if not report.trend:
399
+ return
400
+
401
+ trend_emoji = self._get_trend_emoji(report.trend.direction.value)
402
+ self.console.print(
403
+ f"[dim]{trend_emoji} Trend: {report.trend.direction.value} "
404
+ f"({report.trend.confidence:.1%} confidence)[/dim]"
405
+ )
406
+
407
+ def _get_trend_emoji(self, direction: str) -> str:
408
+ """Get emoji for trend direction."""
409
+ trend_emojis = {
410
+ "improving": "📈",
411
+ "declining": "📉",
412
+ "stable": "📊",
413
+ "volatile": "⚠️",
414
+ }
415
+ return trend_emojis.get(direction, "📊")
416
+
417
+ def _display_critical_alerts(self, report: t.Any) -> None:
418
+ """Display critical quality alerts."""
419
+ critical_alerts = [a for a in report.alerts if a.severity.value == "critical"]
420
+ if critical_alerts:
421
+ self.console.print(
422
+ f"[red]🚨 {len(critical_alerts)} critical quality issues[/red]"
423
+ )
424
+
425
+ def _display_top_recommendations(self, report: t.Any) -> None:
426
+ """Display top quality recommendations."""
427
+ if not report.recommendations:
428
+ return
429
+
430
+ self.console.print("\n[yellow]💡 Top Recommendations:[/yellow]")
431
+ for rec in report.recommendations[:2]: # Show top 2
432
+ self.console.print(f" {rec}")
433
+
434
+ # Silently fail for display issues using suppress above