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.
- crackerjack/CLAUDE.md +1005 -0
- crackerjack/RULES.md +380 -0
- crackerjack/__init__.py +42 -13
- crackerjack/__main__.py +225 -253
- crackerjack/agents/__init__.py +41 -0
- crackerjack/agents/architect_agent.py +281 -0
- crackerjack/agents/base.py +169 -0
- crackerjack/agents/coordinator.py +512 -0
- crackerjack/agents/documentation_agent.py +498 -0
- crackerjack/agents/dry_agent.py +388 -0
- crackerjack/agents/formatting_agent.py +245 -0
- crackerjack/agents/import_optimization_agent.py +281 -0
- crackerjack/agents/performance_agent.py +669 -0
- crackerjack/agents/proactive_agent.py +104 -0
- crackerjack/agents/refactoring_agent.py +788 -0
- crackerjack/agents/security_agent.py +529 -0
- crackerjack/agents/test_creation_agent.py +652 -0
- crackerjack/agents/test_specialist_agent.py +486 -0
- crackerjack/agents/tracker.py +212 -0
- crackerjack/api.py +560 -0
- crackerjack/cli/__init__.py +24 -0
- crackerjack/cli/facade.py +104 -0
- crackerjack/cli/handlers.py +267 -0
- crackerjack/cli/interactive.py +471 -0
- crackerjack/cli/options.py +401 -0
- crackerjack/cli/utils.py +18 -0
- crackerjack/code_cleaner.py +670 -0
- crackerjack/config/__init__.py +19 -0
- crackerjack/config/hooks.py +218 -0
- crackerjack/core/__init__.py +0 -0
- crackerjack/core/async_workflow_orchestrator.py +406 -0
- crackerjack/core/autofix_coordinator.py +200 -0
- crackerjack/core/container.py +104 -0
- crackerjack/core/enhanced_container.py +542 -0
- crackerjack/core/performance.py +243 -0
- crackerjack/core/phase_coordinator.py +561 -0
- crackerjack/core/proactive_workflow.py +316 -0
- crackerjack/core/session_coordinator.py +289 -0
- crackerjack/core/workflow_orchestrator.py +640 -0
- crackerjack/dynamic_config.py +577 -0
- crackerjack/errors.py +263 -41
- crackerjack/executors/__init__.py +11 -0
- crackerjack/executors/async_hook_executor.py +431 -0
- crackerjack/executors/cached_hook_executor.py +242 -0
- crackerjack/executors/hook_executor.py +345 -0
- crackerjack/executors/individual_hook_executor.py +669 -0
- crackerjack/intelligence/__init__.py +44 -0
- crackerjack/intelligence/adaptive_learning.py +751 -0
- crackerjack/intelligence/agent_orchestrator.py +551 -0
- crackerjack/intelligence/agent_registry.py +414 -0
- crackerjack/intelligence/agent_selector.py +502 -0
- crackerjack/intelligence/integration.py +290 -0
- crackerjack/interactive.py +576 -315
- crackerjack/managers/__init__.py +11 -0
- crackerjack/managers/async_hook_manager.py +135 -0
- crackerjack/managers/hook_manager.py +137 -0
- crackerjack/managers/publish_manager.py +411 -0
- crackerjack/managers/test_command_builder.py +151 -0
- crackerjack/managers/test_executor.py +435 -0
- crackerjack/managers/test_manager.py +258 -0
- crackerjack/managers/test_manager_backup.py +1124 -0
- crackerjack/managers/test_progress.py +144 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +336 -0
- crackerjack/mcp/client_runner.py +104 -0
- crackerjack/mcp/context.py +615 -0
- crackerjack/mcp/dashboard.py +636 -0
- crackerjack/mcp/enhanced_progress_monitor.py +479 -0
- crackerjack/mcp/file_monitor.py +336 -0
- crackerjack/mcp/progress_components.py +569 -0
- crackerjack/mcp/progress_monitor.py +949 -0
- crackerjack/mcp/rate_limiter.py +332 -0
- crackerjack/mcp/server.py +22 -0
- crackerjack/mcp/server_core.py +244 -0
- crackerjack/mcp/service_watchdog.py +501 -0
- crackerjack/mcp/state.py +395 -0
- crackerjack/mcp/task_manager.py +257 -0
- crackerjack/mcp/tools/__init__.py +17 -0
- crackerjack/mcp/tools/core_tools.py +249 -0
- crackerjack/mcp/tools/error_analyzer.py +308 -0
- crackerjack/mcp/tools/execution_tools.py +370 -0
- crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
- crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
- crackerjack/mcp/tools/intelligence_tools.py +314 -0
- crackerjack/mcp/tools/monitoring_tools.py +502 -0
- crackerjack/mcp/tools/proactive_tools.py +384 -0
- crackerjack/mcp/tools/progress_tools.py +141 -0
- crackerjack/mcp/tools/utility_tools.py +341 -0
- crackerjack/mcp/tools/workflow_executor.py +360 -0
- crackerjack/mcp/websocket/__init__.py +14 -0
- crackerjack/mcp/websocket/app.py +39 -0
- crackerjack/mcp/websocket/endpoints.py +559 -0
- crackerjack/mcp/websocket/jobs.py +253 -0
- crackerjack/mcp/websocket/server.py +116 -0
- crackerjack/mcp/websocket/websocket_handler.py +78 -0
- crackerjack/mcp/websocket_server.py +10 -0
- crackerjack/models/__init__.py +31 -0
- crackerjack/models/config.py +93 -0
- crackerjack/models/config_adapter.py +230 -0
- crackerjack/models/protocols.py +118 -0
- crackerjack/models/task.py +154 -0
- crackerjack/monitoring/ai_agent_watchdog.py +450 -0
- crackerjack/monitoring/regression_prevention.py +638 -0
- crackerjack/orchestration/__init__.py +0 -0
- crackerjack/orchestration/advanced_orchestrator.py +970 -0
- crackerjack/orchestration/execution_strategies.py +341 -0
- crackerjack/orchestration/test_progress_streamer.py +636 -0
- crackerjack/plugins/__init__.py +15 -0
- crackerjack/plugins/base.py +200 -0
- crackerjack/plugins/hooks.py +246 -0
- crackerjack/plugins/loader.py +335 -0
- crackerjack/plugins/managers.py +259 -0
- crackerjack/py313.py +8 -3
- crackerjack/services/__init__.py +22 -0
- crackerjack/services/cache.py +314 -0
- crackerjack/services/config.py +347 -0
- crackerjack/services/config_integrity.py +99 -0
- crackerjack/services/contextual_ai_assistant.py +516 -0
- crackerjack/services/coverage_ratchet.py +347 -0
- crackerjack/services/debug.py +736 -0
- crackerjack/services/dependency_monitor.py +617 -0
- crackerjack/services/enhanced_filesystem.py +439 -0
- crackerjack/services/file_hasher.py +151 -0
- crackerjack/services/filesystem.py +395 -0
- crackerjack/services/git.py +165 -0
- crackerjack/services/health_metrics.py +611 -0
- crackerjack/services/initialization.py +847 -0
- crackerjack/services/log_manager.py +286 -0
- crackerjack/services/logging.py +174 -0
- crackerjack/services/metrics.py +578 -0
- crackerjack/services/pattern_cache.py +362 -0
- crackerjack/services/pattern_detector.py +515 -0
- crackerjack/services/performance_benchmarks.py +653 -0
- crackerjack/services/security.py +163 -0
- crackerjack/services/server_manager.py +234 -0
- crackerjack/services/smart_scheduling.py +144 -0
- crackerjack/services/tool_version_service.py +61 -0
- crackerjack/services/unified_config.py +437 -0
- crackerjack/services/version_checker.py +248 -0
- crackerjack/slash_commands/__init__.py +14 -0
- crackerjack/slash_commands/init.md +122 -0
- crackerjack/slash_commands/run.md +163 -0
- crackerjack/slash_commands/status.md +127 -0
- crackerjack-0.31.4.dist-info/METADATA +742 -0
- crackerjack-0.31.4.dist-info/RECORD +148 -0
- crackerjack-0.31.4.dist-info/entry_points.txt +2 -0
- crackerjack/.gitignore +0 -34
- crackerjack/.libcst.codemod.yaml +0 -18
- crackerjack/.pdm.toml +0 -1
- crackerjack/.pre-commit-config-ai.yaml +0 -149
- crackerjack/.pre-commit-config-fast.yaml +0 -69
- crackerjack/.pre-commit-config.yaml +0 -114
- crackerjack/crackerjack.py +0 -4140
- crackerjack/pyproject.toml +0 -285
- crackerjack-0.29.0.dist-info/METADATA +0 -1289
- crackerjack-0.29.0.dist-info/RECORD +0 -17
- {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/WHEEL +0 -0
- {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)
|