crackerjack 0.29.0__py3-none-any.whl → 0.31.4__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 (158) hide show
  1. crackerjack/CLAUDE.md +1005 -0
  2. crackerjack/RULES.md +380 -0
  3. crackerjack/__init__.py +42 -13
  4. crackerjack/__main__.py +225 -253
  5. crackerjack/agents/__init__.py +41 -0
  6. crackerjack/agents/architect_agent.py +281 -0
  7. crackerjack/agents/base.py +169 -0
  8. crackerjack/agents/coordinator.py +512 -0
  9. crackerjack/agents/documentation_agent.py +498 -0
  10. crackerjack/agents/dry_agent.py +388 -0
  11. crackerjack/agents/formatting_agent.py +245 -0
  12. crackerjack/agents/import_optimization_agent.py +281 -0
  13. crackerjack/agents/performance_agent.py +669 -0
  14. crackerjack/agents/proactive_agent.py +104 -0
  15. crackerjack/agents/refactoring_agent.py +788 -0
  16. crackerjack/agents/security_agent.py +529 -0
  17. crackerjack/agents/test_creation_agent.py +652 -0
  18. crackerjack/agents/test_specialist_agent.py +486 -0
  19. crackerjack/agents/tracker.py +212 -0
  20. crackerjack/api.py +560 -0
  21. crackerjack/cli/__init__.py +24 -0
  22. crackerjack/cli/facade.py +104 -0
  23. crackerjack/cli/handlers.py +267 -0
  24. crackerjack/cli/interactive.py +471 -0
  25. crackerjack/cli/options.py +401 -0
  26. crackerjack/cli/utils.py +18 -0
  27. crackerjack/code_cleaner.py +670 -0
  28. crackerjack/config/__init__.py +19 -0
  29. crackerjack/config/hooks.py +218 -0
  30. crackerjack/core/__init__.py +0 -0
  31. crackerjack/core/async_workflow_orchestrator.py +406 -0
  32. crackerjack/core/autofix_coordinator.py +200 -0
  33. crackerjack/core/container.py +104 -0
  34. crackerjack/core/enhanced_container.py +542 -0
  35. crackerjack/core/performance.py +243 -0
  36. crackerjack/core/phase_coordinator.py +561 -0
  37. crackerjack/core/proactive_workflow.py +316 -0
  38. crackerjack/core/session_coordinator.py +289 -0
  39. crackerjack/core/workflow_orchestrator.py +640 -0
  40. crackerjack/dynamic_config.py +577 -0
  41. crackerjack/errors.py +263 -41
  42. crackerjack/executors/__init__.py +11 -0
  43. crackerjack/executors/async_hook_executor.py +431 -0
  44. crackerjack/executors/cached_hook_executor.py +242 -0
  45. crackerjack/executors/hook_executor.py +345 -0
  46. crackerjack/executors/individual_hook_executor.py +669 -0
  47. crackerjack/intelligence/__init__.py +44 -0
  48. crackerjack/intelligence/adaptive_learning.py +751 -0
  49. crackerjack/intelligence/agent_orchestrator.py +551 -0
  50. crackerjack/intelligence/agent_registry.py +414 -0
  51. crackerjack/intelligence/agent_selector.py +502 -0
  52. crackerjack/intelligence/integration.py +290 -0
  53. crackerjack/interactive.py +576 -315
  54. crackerjack/managers/__init__.py +11 -0
  55. crackerjack/managers/async_hook_manager.py +135 -0
  56. crackerjack/managers/hook_manager.py +137 -0
  57. crackerjack/managers/publish_manager.py +411 -0
  58. crackerjack/managers/test_command_builder.py +151 -0
  59. crackerjack/managers/test_executor.py +435 -0
  60. crackerjack/managers/test_manager.py +258 -0
  61. crackerjack/managers/test_manager_backup.py +1124 -0
  62. crackerjack/managers/test_progress.py +144 -0
  63. crackerjack/mcp/__init__.py +0 -0
  64. crackerjack/mcp/cache.py +336 -0
  65. crackerjack/mcp/client_runner.py +104 -0
  66. crackerjack/mcp/context.py +615 -0
  67. crackerjack/mcp/dashboard.py +636 -0
  68. crackerjack/mcp/enhanced_progress_monitor.py +479 -0
  69. crackerjack/mcp/file_monitor.py +336 -0
  70. crackerjack/mcp/progress_components.py +569 -0
  71. crackerjack/mcp/progress_monitor.py +949 -0
  72. crackerjack/mcp/rate_limiter.py +332 -0
  73. crackerjack/mcp/server.py +22 -0
  74. crackerjack/mcp/server_core.py +244 -0
  75. crackerjack/mcp/service_watchdog.py +501 -0
  76. crackerjack/mcp/state.py +395 -0
  77. crackerjack/mcp/task_manager.py +257 -0
  78. crackerjack/mcp/tools/__init__.py +17 -0
  79. crackerjack/mcp/tools/core_tools.py +249 -0
  80. crackerjack/mcp/tools/error_analyzer.py +308 -0
  81. crackerjack/mcp/tools/execution_tools.py +370 -0
  82. crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
  83. crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
  84. crackerjack/mcp/tools/intelligence_tools.py +314 -0
  85. crackerjack/mcp/tools/monitoring_tools.py +502 -0
  86. crackerjack/mcp/tools/proactive_tools.py +384 -0
  87. crackerjack/mcp/tools/progress_tools.py +141 -0
  88. crackerjack/mcp/tools/utility_tools.py +341 -0
  89. crackerjack/mcp/tools/workflow_executor.py +360 -0
  90. crackerjack/mcp/websocket/__init__.py +14 -0
  91. crackerjack/mcp/websocket/app.py +39 -0
  92. crackerjack/mcp/websocket/endpoints.py +559 -0
  93. crackerjack/mcp/websocket/jobs.py +253 -0
  94. crackerjack/mcp/websocket/server.py +116 -0
  95. crackerjack/mcp/websocket/websocket_handler.py +78 -0
  96. crackerjack/mcp/websocket_server.py +10 -0
  97. crackerjack/models/__init__.py +31 -0
  98. crackerjack/models/config.py +93 -0
  99. crackerjack/models/config_adapter.py +230 -0
  100. crackerjack/models/protocols.py +118 -0
  101. crackerjack/models/task.py +154 -0
  102. crackerjack/monitoring/ai_agent_watchdog.py +450 -0
  103. crackerjack/monitoring/regression_prevention.py +638 -0
  104. crackerjack/orchestration/__init__.py +0 -0
  105. crackerjack/orchestration/advanced_orchestrator.py +970 -0
  106. crackerjack/orchestration/execution_strategies.py +341 -0
  107. crackerjack/orchestration/test_progress_streamer.py +636 -0
  108. crackerjack/plugins/__init__.py +15 -0
  109. crackerjack/plugins/base.py +200 -0
  110. crackerjack/plugins/hooks.py +246 -0
  111. crackerjack/plugins/loader.py +335 -0
  112. crackerjack/plugins/managers.py +259 -0
  113. crackerjack/py313.py +8 -3
  114. crackerjack/services/__init__.py +22 -0
  115. crackerjack/services/cache.py +314 -0
  116. crackerjack/services/config.py +347 -0
  117. crackerjack/services/config_integrity.py +99 -0
  118. crackerjack/services/contextual_ai_assistant.py +516 -0
  119. crackerjack/services/coverage_ratchet.py +347 -0
  120. crackerjack/services/debug.py +736 -0
  121. crackerjack/services/dependency_monitor.py +617 -0
  122. crackerjack/services/enhanced_filesystem.py +439 -0
  123. crackerjack/services/file_hasher.py +151 -0
  124. crackerjack/services/filesystem.py +395 -0
  125. crackerjack/services/git.py +165 -0
  126. crackerjack/services/health_metrics.py +611 -0
  127. crackerjack/services/initialization.py +847 -0
  128. crackerjack/services/log_manager.py +286 -0
  129. crackerjack/services/logging.py +174 -0
  130. crackerjack/services/metrics.py +578 -0
  131. crackerjack/services/pattern_cache.py +362 -0
  132. crackerjack/services/pattern_detector.py +515 -0
  133. crackerjack/services/performance_benchmarks.py +653 -0
  134. crackerjack/services/security.py +163 -0
  135. crackerjack/services/server_manager.py +234 -0
  136. crackerjack/services/smart_scheduling.py +144 -0
  137. crackerjack/services/tool_version_service.py +61 -0
  138. crackerjack/services/unified_config.py +437 -0
  139. crackerjack/services/version_checker.py +248 -0
  140. crackerjack/slash_commands/__init__.py +14 -0
  141. crackerjack/slash_commands/init.md +122 -0
  142. crackerjack/slash_commands/run.md +163 -0
  143. crackerjack/slash_commands/status.md +127 -0
  144. crackerjack-0.31.4.dist-info/METADATA +742 -0
  145. crackerjack-0.31.4.dist-info/RECORD +148 -0
  146. crackerjack-0.31.4.dist-info/entry_points.txt +2 -0
  147. crackerjack/.gitignore +0 -34
  148. crackerjack/.libcst.codemod.yaml +0 -18
  149. crackerjack/.pdm.toml +0 -1
  150. crackerjack/.pre-commit-config-ai.yaml +0 -149
  151. crackerjack/.pre-commit-config-fast.yaml +0 -69
  152. crackerjack/.pre-commit-config.yaml +0 -114
  153. crackerjack/crackerjack.py +0 -4140
  154. crackerjack/pyproject.toml +0 -285
  155. crackerjack-0.29.0.dist-info/METADATA +0 -1289
  156. crackerjack-0.29.0.dist-info/RECORD +0 -17
  157. {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/WHEEL +0 -0
  158. {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,669 @@
1
+ import asyncio
2
+ import re
3
+ import subprocess
4
+ import time
5
+ import typing as t
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from rich.console import Console
10
+
11
+ from crackerjack.config.hooks import HookDefinition, HookStrategy
12
+ from crackerjack.models.task import HookResult
13
+
14
+
15
+ @dataclass
16
+ class HookProgress:
17
+ hook_name: str
18
+ status: str
19
+ start_time: float
20
+ end_time: float | None = None
21
+ duration: float | None = None
22
+ errors_found: int = 0
23
+ warnings_found: int = 0
24
+ files_processed: int = 0
25
+ lines_processed: int = 0
26
+ output_lines: list[str] | None = None
27
+ error_details: list[dict[str, t.Any]] | None = None
28
+
29
+ def __post_init__(self) -> None:
30
+ if self.output_lines is None:
31
+ self.output_lines = []
32
+ if self.error_details is None:
33
+ self.error_details = []
34
+ if self.end_time and self.start_time:
35
+ self.duration = self.end_time - self.start_time
36
+
37
+ def to_dict(self) -> dict[str, t.Any]:
38
+ return {
39
+ "hook_name": self.hook_name,
40
+ "status": self.status,
41
+ "start_time": self.start_time,
42
+ "end_time": self.end_time,
43
+ "duration": self.duration,
44
+ "errors_found": self.errors_found,
45
+ "warnings_found": self.warnings_found,
46
+ "files_processed": self.files_processed,
47
+ "lines_processed": self.lines_processed,
48
+ "output_lines": self.output_lines[-10:] if self.output_lines else [],
49
+ "error_details": self.error_details,
50
+ }
51
+
52
+
53
+ @dataclass
54
+ class IndividualExecutionResult:
55
+ strategy_name: str
56
+ hook_results: list[HookResult]
57
+ hook_progress: list[HookProgress]
58
+ total_duration: float
59
+ success: bool
60
+ execution_order: list[str]
61
+
62
+ @property
63
+ def failed_hooks(self) -> list[str]:
64
+ return [p.hook_name for p in self.hook_progress if p.status == "failed"]
65
+
66
+ @property
67
+ def total_errors(self) -> int:
68
+ return sum(p.errors_found for p in self.hook_progress)
69
+
70
+ @property
71
+ def total_warnings(self) -> int:
72
+ return sum(p.warnings_found for p in self.hook_progress)
73
+
74
+
75
+ class HookOutputParser:
76
+ HOOK_PATTERNS: dict[str, dict[str, re.Pattern[str]]] = {
77
+ "ruff-check": {
78
+ "error": re.compile(r"^(.+?):(\d+):(\d+):([A-Z]\d+) (.+)$"),
79
+ "summary": re.compile(r"Found (\d+) error"),
80
+ },
81
+ "pyright": {
82
+ "error": re.compile(r"^(.+?):(\d+):(\d+) - error: (.+)$"),
83
+ "warning": re.compile(r"^(.+?):(\d+):(\d+) - warning: (.+)$"),
84
+ "summary": re.compile(r"(\d+) error[s]?, (\d+) warning[s]?"),
85
+ },
86
+ "bandit": {
87
+ "issue": re.compile(r" >> Issue: \[([A-Z]\d+): \w+\] (.+)"),
88
+ "location": re.compile(r" Location: (.+?):(\d+):(\d+)"),
89
+ "confidence": re.compile(r" Confidence: (\w+)"),
90
+ "severity": re.compile(r" Severity: (\w+)"),
91
+ },
92
+ "mypy": {
93
+ "error": re.compile(r"^(.+?):(\d+): error: (.+)$"),
94
+ "note": re.compile(r"^(.+?):(\d+): note: (.+)$"),
95
+ },
96
+ "vulture": {
97
+ "unused": re.compile(r"^(.+?):(\d+): unused (.+) '(.+)'"),
98
+ },
99
+ "complexipy": {
100
+ "complex": re.compile(
101
+ r"^(.+?):(\d+):(\d+) - (.+) is too complex \((\d+)\)",
102
+ ),
103
+ },
104
+ }
105
+
106
+ def parse_hook_output(
107
+ self,
108
+ hook_name: str,
109
+ output_lines: list[str],
110
+ ) -> dict[str, t.Any]:
111
+ if hook_name not in self.HOOK_PATTERNS:
112
+ return self._parse_generic_output(output_lines)
113
+
114
+ result: dict[str, t.Any] = {
115
+ "errors": [],
116
+ "warnings": [],
117
+ "files_processed": set(),
118
+ }
119
+ patterns = self.HOOK_PATTERNS[hook_name]
120
+
121
+ parser_map = {
122
+ "ruff-check": self._parse_ruff_check,
123
+ "pyright": self._parse_pyright,
124
+ "bandit": self._parse_bandit,
125
+ "vulture": self._parse_vulture,
126
+ "complexipy": self._parse_complexipy,
127
+ }
128
+
129
+ parser_map.get(hook_name, self._parse_default_hook)(
130
+ output_lines, patterns, result
131
+ )
132
+
133
+ result["files_processed"] = list(result["files_processed"])
134
+ return result
135
+
136
+ def _parse_ruff_check(
137
+ self,
138
+ output_lines: list[str],
139
+ patterns: dict[str, re.Pattern[str]],
140
+ result: dict[str, t.Any],
141
+ ) -> None:
142
+ for line in output_lines:
143
+ line = line.strip()
144
+ if not line:
145
+ continue
146
+ if match := patterns["error"].match(line):
147
+ file_path, line_num, col_num, code, message = match.groups()
148
+ result["files_processed"].add(file_path)
149
+ result["errors"].append(
150
+ {
151
+ "file": file_path,
152
+ "line": int(line_num),
153
+ "column": int(col_num),
154
+ "code": code,
155
+ "message": message,
156
+ "type": "error",
157
+ },
158
+ )
159
+
160
+ def _parse_pyright(
161
+ self,
162
+ output_lines: list[str],
163
+ patterns: dict[str, re.Pattern[str]],
164
+ result: dict[str, t.Any],
165
+ ) -> None:
166
+ for line in output_lines:
167
+ line = line.strip()
168
+ if not line:
169
+ continue
170
+ if match := patterns["error"].match(line):
171
+ file_path, line_num, col_num, message = match.groups()
172
+ result["files_processed"].add(file_path)
173
+ result["errors"].append(
174
+ {
175
+ "file": file_path,
176
+ "line": int(line_num),
177
+ "column": int(col_num),
178
+ "message": message,
179
+ "type": "error",
180
+ },
181
+ )
182
+ elif match := patterns["warning"].match(line):
183
+ file_path, line_num, col_num, message = match.groups()
184
+ result["files_processed"].add(file_path)
185
+ result["warnings"].append(
186
+ {
187
+ "file": file_path,
188
+ "line": int(line_num),
189
+ "column": int(col_num),
190
+ "message": message,
191
+ "type": "warning",
192
+ },
193
+ )
194
+
195
+ def _parse_bandit(
196
+ self,
197
+ output_lines: list[str],
198
+ patterns: dict[str, re.Pattern[str]],
199
+ result: dict[str, t.Any],
200
+ ) -> None:
201
+ for line in output_lines:
202
+ line = line.strip()
203
+ if not line:
204
+ continue
205
+ if match := patterns["issue"].match(line):
206
+ code, message = match.groups()
207
+ result["errors"].append(
208
+ {"code": code, "message": message, "type": "security"},
209
+ )
210
+
211
+ def _parse_vulture(
212
+ self,
213
+ output_lines: list[str],
214
+ patterns: dict[str, re.Pattern[str]],
215
+ result: dict[str, t.Any],
216
+ ) -> None:
217
+ for line in output_lines:
218
+ line = line.strip()
219
+ if not line:
220
+ continue
221
+ if match := patterns["unused"].match(line):
222
+ file_path, line_num, item_type, item_name = match.groups()
223
+ result["files_processed"].add(file_path)
224
+ result["warnings"].append(
225
+ {
226
+ "file": file_path,
227
+ "line": int(line_num),
228
+ "message": f"unused {item_type} '{item_name}'",
229
+ "type": "unused_code",
230
+ },
231
+ )
232
+
233
+ def _parse_complexipy(
234
+ self,
235
+ output_lines: list[str],
236
+ patterns: dict[str, re.Pattern[str]],
237
+ result: dict[str, t.Any],
238
+ ) -> None:
239
+ for line in output_lines:
240
+ line = line.strip()
241
+ if not line:
242
+ continue
243
+ if match := patterns["complex"].match(line):
244
+ file_path, line_num, col_num, function_name, complexity = match.groups()
245
+ result["files_processed"].add(file_path)
246
+ result["errors"].append(
247
+ {
248
+ "file": file_path,
249
+ "line": int(line_num),
250
+ "column": int(col_num),
251
+ "message": f"{function_name} is too complex ({complexity})",
252
+ "type": "complexity",
253
+ },
254
+ )
255
+
256
+ def _parse_default_hook(
257
+ self,
258
+ output_lines: list[str],
259
+ patterns: dict[str, re.Pattern[str]],
260
+ result: dict[str, t.Any],
261
+ ) -> None:
262
+ # Default parser for hooks not specifically handled
263
+ for line in output_lines:
264
+ line = line.strip()
265
+ if not line:
266
+ continue
267
+ # Simple heuristic - if it looks like an error, treat it as one
268
+ if "error" in line.lower() or "fail" in line.lower():
269
+ result["errors"].append(
270
+ {
271
+ "message": line,
272
+ "type": "generic_error",
273
+ },
274
+ )
275
+ elif "warning" in line.lower():
276
+ result["warnings"].append(
277
+ {
278
+ "message": line,
279
+ "type": "generic_warning",
280
+ },
281
+ )
282
+
283
+ def _parse_generic_output(self, output_lines: list[str]) -> dict[str, t.Any]:
284
+ errors: list[dict[str, str]] = []
285
+ warnings: list[dict[str, str]] = []
286
+
287
+ error_keywords = ["error", "failed", "violation", "issue"]
288
+ warning_keywords = ["warning", "caution", "note"]
289
+
290
+ for line in output_lines:
291
+ line_lower = line.lower()
292
+ if any(keyword in line_lower for keyword in error_keywords):
293
+ errors.append({"message": line.strip(), "type": "generic_error"})
294
+ elif any(keyword in line_lower for keyword in warning_keywords):
295
+ warnings.append({"message": line.strip(), "type": "generic_warning"})
296
+
297
+ return {
298
+ "errors": errors,
299
+ "warnings": warnings,
300
+ "files_processed": 0,
301
+ "total_lines": len(output_lines),
302
+ }
303
+
304
+
305
+ class IndividualHookExecutor:
306
+ def __init__(self, console: Console, pkg_path: Path) -> None:
307
+ self.console = console
308
+ self.pkg_path = pkg_path
309
+ self.parser = HookOutputParser()
310
+ self.progress_callback: t.Callable[[HookProgress], None] | None = None
311
+ self.suppress_realtime_output = False
312
+ self.progress_callback_interval = (
313
+ 1 # Only callback every N lines to reduce overhead
314
+ )
315
+
316
+ def set_progress_callback(self, callback: t.Callable[[HookProgress], None]) -> None:
317
+ self.progress_callback = callback
318
+
319
+ def set_mcp_mode(self, enable: bool = True) -> None:
320
+ """Enable MCP mode which suppresses real-time output to prevent terminal lockup."""
321
+ self.suppress_realtime_output = enable
322
+ if enable:
323
+ self.progress_callback_interval = (
324
+ 10 # Reduce callback frequency in MCP mode
325
+ )
326
+
327
+ async def execute_strategy_individual(
328
+ self,
329
+ strategy: HookStrategy,
330
+ ) -> IndividualExecutionResult:
331
+ """Execute all hooks in a strategy individually (non-parallel)."""
332
+ start_time = time.time()
333
+ self._print_strategy_header(strategy)
334
+
335
+ execution_state = self._initialize_execution_state()
336
+
337
+ for hook in strategy.hooks:
338
+ await self._execute_single_hook_in_strategy(hook, execution_state)
339
+
340
+ return self._finalize_execution_result(strategy, execution_state, start_time)
341
+
342
+ def _initialize_execution_state(self) -> dict[str, t.Any]:
343
+ """Initialize state tracking for strategy execution."""
344
+ return {"hook_results": [], "hook_progress": [], "execution_order": []}
345
+
346
+ async def _execute_single_hook_in_strategy(
347
+ self,
348
+ hook: HookDefinition,
349
+ execution_state: dict[str, t.Any],
350
+ ) -> None:
351
+ """Execute a single hook and update execution state."""
352
+ execution_state["execution_order"].append(hook.name)
353
+
354
+ progress = HookProgress(
355
+ hook_name=hook.name,
356
+ status="pending",
357
+ start_time=time.time(),
358
+ )
359
+ execution_state["hook_progress"].append(progress)
360
+
361
+ result = await self._execute_individual_hook(hook, progress)
362
+ execution_state["hook_results"].append(result)
363
+
364
+ self._update_hook_progress_status(progress, result)
365
+
366
+ def _update_hook_progress_status(
367
+ self,
368
+ progress: HookProgress,
369
+ result: HookResult,
370
+ ) -> None:
371
+ """Update progress status after hook execution."""
372
+ progress.status = "completed" if result.status == "passed" else "failed"
373
+ progress.end_time = time.time()
374
+ progress.duration = progress.end_time - progress.start_time
375
+
376
+ if self.progress_callback:
377
+ self.progress_callback(progress)
378
+
379
+ def _finalize_execution_result(
380
+ self,
381
+ strategy: HookStrategy,
382
+ execution_state: dict[str, t.Any],
383
+ start_time: float,
384
+ ) -> IndividualExecutionResult:
385
+ """Finalize and return the execution result."""
386
+ total_duration = time.time() - start_time
387
+ success = all(r.status == "passed" for r in execution_state["hook_results"])
388
+
389
+ self._print_individual_summary(
390
+ strategy,
391
+ execution_state["hook_results"],
392
+ execution_state["hook_progress"],
393
+ )
394
+
395
+ return IndividualExecutionResult(
396
+ strategy_name=f"{strategy.name}_individual",
397
+ hook_results=execution_state["hook_results"],
398
+ hook_progress=execution_state["hook_progress"],
399
+ total_duration=total_duration,
400
+ success=success,
401
+ execution_order=execution_state["execution_order"],
402
+ )
403
+
404
+ async def _execute_individual_hook(
405
+ self,
406
+ hook: HookDefinition,
407
+ progress: HookProgress,
408
+ ) -> HookResult:
409
+ progress.status = "running"
410
+ if self.progress_callback:
411
+ self.progress_callback(progress)
412
+
413
+ self.console.print(f"\n[bold cyan]🔍 Running {hook.name}[/bold cyan]")
414
+
415
+ cmd = hook.get_command()
416
+
417
+ try:
418
+ result = await self._run_command_with_streaming(cmd, hook.timeout, progress)
419
+
420
+ parsed_output = self.parser.parse_hook_output(
421
+ hook.name,
422
+ progress.output_lines or [],
423
+ )
424
+ progress.errors_found = len(parsed_output["errors"])
425
+ progress.warnings_found = len(parsed_output["warnings"])
426
+ progress.files_processed = parsed_output["files_processed"]
427
+ progress.lines_processed = parsed_output["total_lines"]
428
+ progress.error_details = parsed_output["errors"] + parsed_output["warnings"]
429
+
430
+ hook_result = HookResult(
431
+ id=hook.name,
432
+ name=hook.name,
433
+ status="passed" if result.returncode == 0 else "failed",
434
+ duration=progress.duration or 0,
435
+ )
436
+
437
+ self._print_hook_summary(hook.name, hook_result, progress)
438
+
439
+ return hook_result
440
+
441
+ except TimeoutError:
442
+ progress.status = "failed"
443
+ error_msg = f"Hook {hook.name} timed out after {hook.timeout}s"
444
+ self.console.print(f"[red]⏰ {error_msg}[/red]")
445
+
446
+ return HookResult(
447
+ id=hook.name,
448
+ name=hook.name,
449
+ status="failed",
450
+ duration=hook.timeout,
451
+ )
452
+
453
+ async def _run_command_with_streaming(
454
+ self,
455
+ cmd: list[str],
456
+ timeout: int,
457
+ progress: HookProgress,
458
+ ) -> subprocess.CompletedProcess[str]:
459
+ """Run command with streaming output and progress tracking."""
460
+ process = await self._create_subprocess(cmd)
461
+
462
+ stdout_lines: list[str] = []
463
+ stderr_lines: list[str] = []
464
+
465
+ tasks = self._create_stream_reader_tasks(
466
+ process,
467
+ stdout_lines,
468
+ stderr_lines,
469
+ progress,
470
+ )
471
+
472
+ try:
473
+ await self._wait_for_process_completion(process, tasks, timeout)
474
+ except TimeoutError:
475
+ self._handle_process_timeout(process, tasks)
476
+ raise
477
+
478
+ return self._create_completed_process(cmd, process, stdout_lines, stderr_lines)
479
+
480
+ async def _create_subprocess(self, cmd: list[str]) -> asyncio.subprocess.Process:
481
+ """Create subprocess for command execution."""
482
+ return await asyncio.create_subprocess_exec(
483
+ *cmd,
484
+ cwd=self.pkg_path,
485
+ stdout=asyncio.subprocess.PIPE,
486
+ stderr=asyncio.subprocess.PIPE,
487
+ )
488
+
489
+ def _create_stream_reader_tasks(
490
+ self,
491
+ process: asyncio.subprocess.Process,
492
+ stdout_lines: list[str],
493
+ stderr_lines: list[str],
494
+ progress: HookProgress,
495
+ ) -> list[asyncio.Task[None]]:
496
+ """Create tasks for reading stdout and stderr streams."""
497
+ return [
498
+ asyncio.create_task(
499
+ self._read_stream(process.stdout, stdout_lines, progress),
500
+ ),
501
+ asyncio.create_task(
502
+ self._read_stream(process.stderr, stderr_lines, progress),
503
+ ),
504
+ ]
505
+
506
+ async def _read_stream(
507
+ self,
508
+ stream: asyncio.StreamReader | None,
509
+ output_list: list[str],
510
+ progress: HookProgress,
511
+ ) -> None:
512
+ """Read lines from stream and update progress."""
513
+ if not stream:
514
+ return
515
+
516
+ line_count = 0
517
+ while True:
518
+ try:
519
+ line = await stream.readline()
520
+ if not line:
521
+ break
522
+
523
+ line_str = self._process_stream_line(line)
524
+ self._update_progress_with_line(
525
+ line_str,
526
+ output_list,
527
+ progress,
528
+ line_count,
529
+ )
530
+ line_count += 1
531
+
532
+ except Exception:
533
+ break
534
+
535
+ def _process_stream_line(self, line: bytes | str) -> str:
536
+ """Process a line from stream into clean string."""
537
+ return (line.decode() if isinstance(line, bytes) else line).rstrip()
538
+
539
+ def _update_progress_with_line(
540
+ self,
541
+ line_str: str,
542
+ output_list: list[str],
543
+ progress: HookProgress,
544
+ line_count: int,
545
+ ) -> None:
546
+ """Update progress tracking with new line."""
547
+ output_list.append(line_str)
548
+ progress.output_lines = progress.output_lines or []
549
+ progress.output_lines.append(line_str)
550
+
551
+ self._maybe_print_line(line_str)
552
+ self._maybe_callback_progress(progress, line_count)
553
+
554
+ def _maybe_print_line(self, line_str: str) -> None:
555
+ """Print line to console if not suppressed."""
556
+ if not self.suppress_realtime_output and line_str.strip():
557
+ self.console.print(f"[dim] {line_str}[/dim]")
558
+
559
+ def _maybe_callback_progress(self, progress: HookProgress, line_count: int) -> None:
560
+ """Callback progress if conditions are met."""
561
+ if self.progress_callback and (
562
+ line_count % self.progress_callback_interval == 0
563
+ ):
564
+ self.progress_callback(progress)
565
+
566
+ async def _wait_for_process_completion(
567
+ self,
568
+ process: asyncio.subprocess.Process,
569
+ tasks: list[asyncio.Task[None]],
570
+ timeout: int,
571
+ ) -> None:
572
+ """Wait for process completion with timeout."""
573
+ await asyncio.wait_for(process.wait(), timeout=timeout)
574
+ await asyncio.gather(*tasks, return_exceptions=True)
575
+
576
+ def _handle_process_timeout(
577
+ self,
578
+ process: asyncio.subprocess.Process,
579
+ tasks: list[asyncio.Task[None]],
580
+ ) -> None:
581
+ """Handle process timeout by killing process and canceling tasks."""
582
+ process.kill()
583
+ for task in tasks:
584
+ task.cancel()
585
+
586
+ def _create_completed_process(
587
+ self,
588
+ cmd: list[str],
589
+ process: asyncio.subprocess.Process,
590
+ stdout_lines: list[str],
591
+ stderr_lines: list[str],
592
+ ) -> subprocess.CompletedProcess[str]:
593
+ """Create CompletedProcess result."""
594
+ return subprocess.CompletedProcess(
595
+ args=cmd,
596
+ returncode=process.returncode or 0,
597
+ stdout="\n".join(stdout_lines),
598
+ stderr="\n".join(stderr_lines),
599
+ )
600
+
601
+ def _print_strategy_header(self, strategy: HookStrategy) -> None:
602
+ self.console.print("\n" + "=" * 80)
603
+ self.console.print(
604
+ f"[bold bright_cyan]🔍 INDIVIDUAL HOOK EXECUTION[/bold bright_cyan] "
605
+ f"[bold bright_white]{strategy.name.upper()} HOOKS[/bold bright_white]",
606
+ )
607
+ self.console.print(
608
+ f"[dim]Running {len(strategy.hooks)} hooks individually with real-time streaming[/dim]",
609
+ )
610
+ self.console.print("=" * 80)
611
+
612
+ def _print_hook_summary(
613
+ self,
614
+ hook_name: str,
615
+ result: HookResult,
616
+ progress: HookProgress,
617
+ ) -> None:
618
+ status_icon = "✅" if result.status == "passed" else "❌"
619
+ duration_str = f"{progress.duration:.1f}s" if progress.duration else "0.0s"
620
+
621
+ summary_parts: list[str] = []
622
+ if progress.errors_found > 0:
623
+ summary_parts.append(f"{progress.errors_found} errors")
624
+ if progress.warnings_found > 0:
625
+ summary_parts.append(f"{progress.warnings_found} warnings")
626
+ if progress.files_processed > 0:
627
+ summary_parts.append(f"{progress.files_processed} files")
628
+
629
+ summary = ", ".join(summary_parts) if summary_parts else "clean"
630
+
631
+ self.console.print(
632
+ f"[bold]{status_icon} {hook_name}[/bold] - {duration_str} - {summary}",
633
+ )
634
+
635
+ def _print_individual_summary(
636
+ self,
637
+ strategy: HookStrategy,
638
+ results: list[HookResult],
639
+ progress_list: list[HookProgress],
640
+ ) -> None:
641
+ passed = sum(1 for r in results if r.status == "passed")
642
+ failed = sum(1 for r in results if r.status == "failed")
643
+ total_errors = sum(p.errors_found for p in progress_list)
644
+ total_warnings = sum(p.warnings_found for p in progress_list)
645
+ total_duration = sum(p.duration or 0 for p in progress_list)
646
+
647
+ self.console.print("\n" + "-" * 80)
648
+ self.console.print(
649
+ f"[bold]📊 INDIVIDUAL EXECUTION SUMMARY[/bold] - {strategy.name.upper()}",
650
+ )
651
+ self.console.print(f"✅ Passed: {passed} | ❌ Failed: {failed}")
652
+ if total_errors > 0:
653
+ self.console.print(f"🚨 Total Errors: {total_errors}")
654
+ if total_warnings > 0:
655
+ self.console.print(f"⚠️ Total Warnings: {total_warnings}")
656
+ self.console.print(f"⏱️ Total Duration: {total_duration:.1f}s")
657
+
658
+ if failed > 0:
659
+ self.console.print("\n[bold red]Failed Hooks: [/bold red]")
660
+ for progress in progress_list:
661
+ if progress.status == "failed":
662
+ error_summary = (
663
+ f"{progress.errors_found} errors"
664
+ if progress.errors_found > 0
665
+ else "failed"
666
+ )
667
+ self.console.print(f" ❌ {progress.hook_name} - {error_summary}")
668
+
669
+ self.console.print("-" * 80)