crackerjack 0.31.10__py3-none-any.whl → 0.31.13__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 (155) hide show
  1. crackerjack/CLAUDE.md +288 -705
  2. crackerjack/__main__.py +22 -8
  3. crackerjack/agents/__init__.py +0 -3
  4. crackerjack/agents/architect_agent.py +0 -43
  5. crackerjack/agents/base.py +1 -9
  6. crackerjack/agents/coordinator.py +2 -148
  7. crackerjack/agents/documentation_agent.py +109 -81
  8. crackerjack/agents/dry_agent.py +122 -97
  9. crackerjack/agents/formatting_agent.py +3 -16
  10. crackerjack/agents/import_optimization_agent.py +1174 -130
  11. crackerjack/agents/performance_agent.py +956 -188
  12. crackerjack/agents/performance_helpers.py +229 -0
  13. crackerjack/agents/proactive_agent.py +1 -48
  14. crackerjack/agents/refactoring_agent.py +516 -246
  15. crackerjack/agents/refactoring_helpers.py +282 -0
  16. crackerjack/agents/security_agent.py +393 -90
  17. crackerjack/agents/test_creation_agent.py +1776 -120
  18. crackerjack/agents/test_specialist_agent.py +59 -15
  19. crackerjack/agents/tracker.py +0 -102
  20. crackerjack/api.py +145 -37
  21. crackerjack/cli/handlers.py +48 -30
  22. crackerjack/cli/interactive.py +11 -11
  23. crackerjack/cli/options.py +66 -4
  24. crackerjack/code_cleaner.py +808 -148
  25. crackerjack/config/global_lock_config.py +110 -0
  26. crackerjack/config/hooks.py +43 -64
  27. crackerjack/core/async_workflow_orchestrator.py +247 -97
  28. crackerjack/core/autofix_coordinator.py +192 -109
  29. crackerjack/core/enhanced_container.py +46 -63
  30. crackerjack/core/file_lifecycle.py +549 -0
  31. crackerjack/core/performance.py +9 -8
  32. crackerjack/core/performance_monitor.py +395 -0
  33. crackerjack/core/phase_coordinator.py +281 -94
  34. crackerjack/core/proactive_workflow.py +9 -58
  35. crackerjack/core/resource_manager.py +501 -0
  36. crackerjack/core/service_watchdog.py +490 -0
  37. crackerjack/core/session_coordinator.py +4 -8
  38. crackerjack/core/timeout_manager.py +504 -0
  39. crackerjack/core/websocket_lifecycle.py +475 -0
  40. crackerjack/core/workflow_orchestrator.py +343 -209
  41. crackerjack/dynamic_config.py +50 -9
  42. crackerjack/errors.py +3 -4
  43. crackerjack/executors/async_hook_executor.py +63 -13
  44. crackerjack/executors/cached_hook_executor.py +14 -14
  45. crackerjack/executors/hook_executor.py +100 -37
  46. crackerjack/executors/hook_lock_manager.py +856 -0
  47. crackerjack/executors/individual_hook_executor.py +120 -86
  48. crackerjack/intelligence/__init__.py +0 -7
  49. crackerjack/intelligence/adaptive_learning.py +13 -86
  50. crackerjack/intelligence/agent_orchestrator.py +15 -78
  51. crackerjack/intelligence/agent_registry.py +12 -59
  52. crackerjack/intelligence/agent_selector.py +31 -92
  53. crackerjack/intelligence/integration.py +1 -41
  54. crackerjack/interactive.py +9 -9
  55. crackerjack/managers/async_hook_manager.py +25 -8
  56. crackerjack/managers/hook_manager.py +9 -9
  57. crackerjack/managers/publish_manager.py +57 -59
  58. crackerjack/managers/test_command_builder.py +6 -36
  59. crackerjack/managers/test_executor.py +9 -61
  60. crackerjack/managers/test_manager.py +17 -63
  61. crackerjack/managers/test_manager_backup.py +77 -127
  62. crackerjack/managers/test_progress.py +4 -23
  63. crackerjack/mcp/cache.py +5 -12
  64. crackerjack/mcp/client_runner.py +10 -10
  65. crackerjack/mcp/context.py +64 -6
  66. crackerjack/mcp/dashboard.py +14 -11
  67. crackerjack/mcp/enhanced_progress_monitor.py +55 -55
  68. crackerjack/mcp/file_monitor.py +72 -42
  69. crackerjack/mcp/progress_components.py +103 -84
  70. crackerjack/mcp/progress_monitor.py +122 -49
  71. crackerjack/mcp/rate_limiter.py +12 -12
  72. crackerjack/mcp/server_core.py +16 -22
  73. crackerjack/mcp/service_watchdog.py +26 -26
  74. crackerjack/mcp/state.py +15 -0
  75. crackerjack/mcp/tools/core_tools.py +95 -39
  76. crackerjack/mcp/tools/error_analyzer.py +6 -32
  77. crackerjack/mcp/tools/execution_tools.py +1 -56
  78. crackerjack/mcp/tools/execution_tools_backup.py +35 -131
  79. crackerjack/mcp/tools/intelligence_tool_registry.py +0 -36
  80. crackerjack/mcp/tools/intelligence_tools.py +2 -55
  81. crackerjack/mcp/tools/monitoring_tools.py +308 -145
  82. crackerjack/mcp/tools/proactive_tools.py +12 -42
  83. crackerjack/mcp/tools/progress_tools.py +23 -15
  84. crackerjack/mcp/tools/utility_tools.py +3 -40
  85. crackerjack/mcp/tools/workflow_executor.py +40 -60
  86. crackerjack/mcp/websocket/app.py +0 -3
  87. crackerjack/mcp/websocket/endpoints.py +206 -268
  88. crackerjack/mcp/websocket/jobs.py +213 -66
  89. crackerjack/mcp/websocket/server.py +84 -6
  90. crackerjack/mcp/websocket/websocket_handler.py +137 -29
  91. crackerjack/models/config_adapter.py +3 -16
  92. crackerjack/models/protocols.py +162 -3
  93. crackerjack/models/resource_protocols.py +454 -0
  94. crackerjack/models/task.py +3 -3
  95. crackerjack/monitoring/__init__.py +0 -0
  96. crackerjack/monitoring/ai_agent_watchdog.py +25 -71
  97. crackerjack/monitoring/regression_prevention.py +28 -87
  98. crackerjack/orchestration/advanced_orchestrator.py +44 -78
  99. crackerjack/orchestration/coverage_improvement.py +10 -60
  100. crackerjack/orchestration/execution_strategies.py +16 -16
  101. crackerjack/orchestration/test_progress_streamer.py +61 -53
  102. crackerjack/plugins/base.py +1 -1
  103. crackerjack/plugins/managers.py +22 -20
  104. crackerjack/py313.py +65 -21
  105. crackerjack/services/backup_service.py +467 -0
  106. crackerjack/services/bounded_status_operations.py +627 -0
  107. crackerjack/services/cache.py +7 -9
  108. crackerjack/services/config.py +35 -52
  109. crackerjack/services/config_integrity.py +5 -16
  110. crackerjack/services/config_merge.py +542 -0
  111. crackerjack/services/contextual_ai_assistant.py +17 -19
  112. crackerjack/services/coverage_ratchet.py +44 -73
  113. crackerjack/services/debug.py +25 -39
  114. crackerjack/services/dependency_monitor.py +52 -50
  115. crackerjack/services/enhanced_filesystem.py +14 -11
  116. crackerjack/services/file_hasher.py +1 -1
  117. crackerjack/services/filesystem.py +1 -12
  118. crackerjack/services/git.py +71 -47
  119. crackerjack/services/health_metrics.py +31 -27
  120. crackerjack/services/initialization.py +276 -428
  121. crackerjack/services/input_validator.py +760 -0
  122. crackerjack/services/log_manager.py +16 -16
  123. crackerjack/services/logging.py +7 -6
  124. crackerjack/services/metrics.py +43 -43
  125. crackerjack/services/pattern_cache.py +2 -31
  126. crackerjack/services/pattern_detector.py +26 -63
  127. crackerjack/services/performance_benchmarks.py +20 -45
  128. crackerjack/services/regex_patterns.py +2887 -0
  129. crackerjack/services/regex_utils.py +537 -0
  130. crackerjack/services/secure_path_utils.py +683 -0
  131. crackerjack/services/secure_status_formatter.py +534 -0
  132. crackerjack/services/secure_subprocess.py +605 -0
  133. crackerjack/services/security.py +47 -10
  134. crackerjack/services/security_logger.py +492 -0
  135. crackerjack/services/server_manager.py +109 -50
  136. crackerjack/services/smart_scheduling.py +8 -25
  137. crackerjack/services/status_authentication.py +603 -0
  138. crackerjack/services/status_security_manager.py +442 -0
  139. crackerjack/services/thread_safe_status_collector.py +546 -0
  140. crackerjack/services/tool_version_service.py +1 -23
  141. crackerjack/services/unified_config.py +36 -58
  142. crackerjack/services/validation_rate_limiter.py +269 -0
  143. crackerjack/services/version_checker.py +9 -40
  144. crackerjack/services/websocket_resource_limiter.py +572 -0
  145. crackerjack/slash_commands/__init__.py +52 -2
  146. crackerjack/tools/__init__.py +0 -0
  147. crackerjack/tools/validate_input_validator_patterns.py +262 -0
  148. crackerjack/tools/validate_regex_patterns.py +198 -0
  149. {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.dist-info}/METADATA +197 -12
  150. crackerjack-0.31.13.dist-info/RECORD +178 -0
  151. crackerjack/cli/facade.py +0 -104
  152. crackerjack-0.31.10.dist-info/RECORD +0 -149
  153. {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.dist-info}/WHEEL +0 -0
  154. {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.dist-info}/entry_points.txt +0 -0
  155. {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.dist-info}/licenses/LICENSE +0 -0
@@ -5,6 +5,30 @@ from pathlib import Path
5
5
  import jinja2
6
6
 
7
7
 
8
+ def _get_project_root() -> Path:
9
+ """Get the absolute path to the crackerjack project root."""
10
+ # Start from this file and go up to find the real project root
11
+ current_path = Path(__file__).resolve()
12
+ for parent in current_path.parents:
13
+ if (parent / "pyproject.toml").exists() and (parent / "tools").exists():
14
+ # Verify it's the crackerjack project by checking for unique markers and tools dir
15
+ try:
16
+ pyproject_content = (parent / "pyproject.toml").read_text()
17
+ if (
18
+ 'name = "crackerjack"' in pyproject_content
19
+ and (
20
+ parent / "tools" / "validate_regex_patterns_standalone.py"
21
+ ).exists()
22
+ ):
23
+ return parent
24
+ except Exception:
25
+ # If we can't read the file, continue searching
26
+ continue
27
+
28
+ # Fallback to the parent of the crackerjack package directory
29
+ return Path(__file__).resolve().parent.parent
30
+
31
+
8
32
  class HookMetadata(t.TypedDict):
9
33
  id: str
10
34
  name: str | None
@@ -32,6 +56,23 @@ class ConfigMode(t.TypedDict):
32
56
 
33
57
  HOOKS_REGISTRY: dict[str, list[HookMetadata]] = {
34
58
  "structure": [
59
+ {
60
+ "id": "validate-regex-patterns",
61
+ "name": "validate-regex-patterns",
62
+ "repo": "local",
63
+ "rev": "",
64
+ "tier": 1,
65
+ "time_estimate": 0.3,
66
+ "stages": None,
67
+ "args": None,
68
+ "files": r"\.py$",
69
+ "exclude": r"^\.venv/",
70
+ "additional_dependencies": None,
71
+ "types_or": None,
72
+ "language": "system",
73
+ "entry": f"uv run python {_get_project_root() / 'tools' / 'validate_regex_patterns_standalone.py'}",
74
+ "experimental": False,
75
+ },
35
76
  {
36
77
  "id": "trailing-whitespace",
37
78
  "name": "trailing-whitespace",
@@ -123,12 +164,12 @@ HOOKS_REGISTRY: dict[str, list[HookMetadata]] = {
123
164
  "id": "uv-lock",
124
165
  "name": None,
125
166
  "repo": "https://github.com/astral-sh/uv-pre-commit",
126
- "rev": "0.8.14",
167
+ "rev": "0.8.15",
127
168
  "tier": 1,
128
169
  "time_estimate": 0.5,
129
170
  "stages": None,
130
171
  "args": None,
131
- "files": r"^pyproject\.toml$",
172
+ "files": r"^ pyproject\.toml$",
132
173
  "exclude": None,
133
174
  "additional_dependencies": None,
134
175
  "types_or": None,
@@ -195,7 +236,7 @@ HOOKS_REGISTRY: dict[str, list[HookMetadata]] = {
195
236
  "id": "ruff-check",
196
237
  "name": None,
197
238
  "repo": "https://github.com/astral-sh/ruff-pre-commit",
198
- "rev": "v0.12.11",
239
+ "rev": "v0.12.12",
199
240
  "tier": 2,
200
241
  "time_estimate": 1.5,
201
242
  "stages": None,
@@ -212,7 +253,7 @@ HOOKS_REGISTRY: dict[str, list[HookMetadata]] = {
212
253
  "id": "ruff-format",
213
254
  "name": None,
214
255
  "repo": "https://github.com/astral-sh/ruff-pre-commit",
215
- "rev": "v0.12.11",
256
+ "rev": "v0.12.12",
216
257
  "tier": 2,
217
258
  "time_estimate": 1.0,
218
259
  "stages": None,
@@ -284,9 +325,9 @@ HOOKS_REGISTRY: dict[str, list[HookMetadata]] = {
284
325
  "repo": "https://github.com/rohaquinlop/complexipy-pre-commit",
285
326
  "rev": "v3.3.0",
286
327
  "tier": 3,
287
- "time_estimate": 2.0,
328
+ "time_estimate": 1.0,
288
329
  "stages": ["pre-push", "manual"],
289
- "args": ["-d", "low"],
330
+ "args": ["-d", "low", "--max-complexity-allowed", "15"],
290
331
  "files": r"^crackerjack/.*\.py$",
291
332
  "exclude": r"^(\.venv/|tests/)",
292
333
  "additional_dependencies": None,
@@ -316,7 +357,7 @@ HOOKS_REGISTRY: dict[str, list[HookMetadata]] = {
316
357
  "id": "pyright",
317
358
  "name": None,
318
359
  "repo": "https://github.com/RobertCraigie/pyright-python",
319
- "rev": "v1.1.404",
360
+ "rev": "v1.1.405",
320
361
  "tier": 3,
321
362
  "time_estimate": 5.0,
322
363
  "stages": ["pre-push", "manual"],
@@ -392,7 +433,7 @@ CONFIG_MODES: dict[str, ConfigMode] = {
392
433
  PRE_COMMIT_TEMPLATE = """repos:
393
434
  {%- for repo in repos %}
394
435
  {%- if repo.comment %}
395
- # {{ repo.comment }}
436
+
396
437
  {%- endif %}
397
438
  - repo: {{ repo.repo }}
398
439
  {%- if repo.rev %}
@@ -430,7 +471,7 @@ PRE_COMMIT_TEMPLATE = """repos:
430
471
  {%- endif %}
431
472
  {%- endfor %}
432
473
 
433
- {%- endfor %}
474
+ {%-endfor %}
434
475
  """
435
476
 
436
477
 
crackerjack/errors.py CHANGED
@@ -314,9 +314,9 @@ def _format_error_for_console(error: CrackerjackError, verbose: bool) -> Panel:
314
314
  content = [error.message]
315
315
 
316
316
  if verbose and error.details:
317
- content.extend(("\n[white]Details: [/white]", str(error.details)))
317
+ content.extend(("\n[white]Details: [/ white]", str(error.details)))
318
318
  if error.recovery:
319
- content.extend(("\n[green]Recovery suggestion: [/green]", str(error.recovery)))
319
+ content.extend(("\n[green]Recovery suggestion: [/ green]", str(error.recovery)))
320
320
 
321
321
  return Panel(
322
322
  "\n".join(content),
@@ -336,7 +336,7 @@ def handle_error(
336
336
  ) -> None:
337
337
  if ai_agent:
338
338
  formatted_json = _format_error_for_ai_agent(error, verbose)
339
- console.print(f"[json]{formatted_json}[/json]")
339
+ console.print(f"[json]{formatted_json}[/ json]")
340
340
  else:
341
341
  panel = _format_error_for_console(error, verbose)
342
342
  console.print(panel)
@@ -375,5 +375,4 @@ def check_command_result(
375
375
 
376
376
 
377
377
  def format_error_report(error: CrackerjackError) -> str:
378
- """Format an error for reporting purposes."""
379
378
  return f"Error {error.error_code.value}: {error.message}"
@@ -7,6 +7,7 @@ from pathlib import Path
7
7
  from rich.console import Console
8
8
 
9
9
  from crackerjack.config.hooks import HookDefinition, HookStrategy, RetryPolicy
10
+ from crackerjack.models.protocols import HookLockManagerProtocol
10
11
  from crackerjack.models.task import HookResult
11
12
  from crackerjack.services.logging import LoggingContext, get_logger
12
13
 
@@ -58,6 +59,7 @@ class AsyncHookExecutor:
58
59
  max_concurrent: int = 4,
59
60
  timeout: int = 300,
60
61
  quiet: bool = False,
62
+ hook_lock_manager: HookLockManagerProtocol | None = None,
61
63
  ) -> None:
62
64
  self.console = console
63
65
  self.pkg_path = pkg_path
@@ -68,6 +70,17 @@ class AsyncHookExecutor:
68
70
 
69
71
  self._semaphore = asyncio.Semaphore(max_concurrent)
70
72
 
73
+ # Use dependency injection for hook lock manager
74
+ if hook_lock_manager is None:
75
+ # Import here to avoid circular imports
76
+ from crackerjack.executors.hook_lock_manager import (
77
+ hook_lock_manager as default_manager,
78
+ )
79
+
80
+ self.hook_lock_manager = default_manager
81
+ else:
82
+ self.hook_lock_manager = hook_lock_manager
83
+
71
84
  async def execute_strategy(
72
85
  self,
73
86
  strategy: HookStrategy,
@@ -129,19 +142,34 @@ class AsyncHookExecutor:
129
142
  performance_gain=performance_gain,
130
143
  )
131
144
 
145
+ def get_lock_statistics(self) -> dict[str, t.Any]:
146
+ """Get comprehensive lock usage statistics for monitoring."""
147
+ return self.hook_lock_manager.get_lock_stats()
148
+
149
+ def get_comprehensive_status(self) -> dict[str, t.Any]:
150
+ """Get comprehensive status including lock manager status."""
151
+ return {
152
+ "executor_config": {
153
+ "max_concurrent": self.max_concurrent,
154
+ "timeout": self.timeout,
155
+ "quiet": self.quiet,
156
+ },
157
+ "lock_manager_status": self.hook_lock_manager.get_lock_stats(),
158
+ }
159
+
132
160
  def _print_strategy_header(self, strategy: HookStrategy) -> None:
133
161
  self.console.print("\n" + "-" * 80)
134
162
  if strategy.name == "fast":
135
163
  self.console.print(
136
- "[bold bright_cyan]🔍 HOOKS[/bold bright_cyan] [bold bright_white]Running code quality checks (async)[/bold bright_white]",
164
+ "[bold bright_cyan]🔍 HOOKS[/ bold bright_cyan] [bold bright_white]Running code quality checks (async)[/ bold bright_white]",
137
165
  )
138
166
  elif strategy.name == "comprehensive":
139
167
  self.console.print(
140
- "[bold bright_cyan]🔍 HOOKS[/bold bright_cyan] [bold bright_white]Running comprehensive quality checks (async)[/bold bright_white]",
168
+ "[bold bright_cyan]🔍 HOOKS[/ bold bright_cyan] [bold bright_white]Running comprehensive quality checks (async)[/ bold bright_white]",
141
169
  )
142
170
  else:
143
171
  self.console.print(
144
- f"[bold bright_cyan]🔍 HOOKS[/bold bright_cyan] [bold bright_white]Running {strategy.name} hooks (async)[/bold bright_white]",
172
+ f"[bold bright_cyan]🔍 HOOKS[/ bold bright_cyan] [bold bright_white]Running {strategy.name} hooks (async)[/ bold bright_white]",
145
173
  )
146
174
  self.console.print("-" * 80 + "\n")
147
175
 
@@ -194,7 +222,30 @@ class AsyncHookExecutor:
194
222
 
195
223
  async def _execute_single_hook(self, hook: HookDefinition) -> HookResult:
196
224
  async with self._semaphore:
197
- return await self._run_hook_subprocess(hook)
225
+ # Check if hook requires sequential execution
226
+ if self.hook_lock_manager.requires_lock(hook.name):
227
+ self.logger.debug(
228
+ f"Hook {hook.name} requires sequential execution lock"
229
+ )
230
+ if not self.quiet:
231
+ self.console.print(
232
+ f"[dim]🔒 {hook.name} (sequential execution)[/dim]"
233
+ )
234
+
235
+ # Acquire hook-specific lock if required (e.g., for complexipy)
236
+ if self.hook_lock_manager.requires_lock(hook.name):
237
+ self.logger.debug(
238
+ f"Hook {hook.name} requires sequential execution lock"
239
+ )
240
+ if not self.quiet:
241
+ self.console.print(
242
+ f"[dim]🔒 {hook.name} (sequential execution)[/dim]"
243
+ )
244
+
245
+ async with self.hook_lock_manager.acquire_hook_lock(hook.name): # type: ignore
246
+ return await self._run_hook_subprocess(hook)
247
+ else:
248
+ return await self._run_hook_subprocess(hook)
198
249
 
199
250
  async def _run_hook_subprocess(self, hook: HookDefinition) -> HookResult:
200
251
  start_time = time.time()
@@ -210,9 +261,15 @@ class AsyncHookExecutor:
210
261
  timeout=timeout_val,
211
262
  )
212
263
 
264
+ # Pre-commit must run from repository root, not package directory
265
+ repo_root = (
266
+ self.pkg_path.parent
267
+ if self.pkg_path.name == "crackerjack"
268
+ else self.pkg_path
269
+ )
213
270
  process = await asyncio.create_subprocess_exec(
214
271
  *cmd,
215
- cwd=self.pkg_path,
272
+ cwd=repo_root,
216
273
  stdout=asyncio.subprocess.PIPE,
217
274
  stderr=asyncio.subprocess.PIPE,
218
275
  )
@@ -419,13 +476,6 @@ class AsyncHookExecutor:
419
476
  ) -> None:
420
477
  if success:
421
478
  self.console.print(
422
- f"[green]✅[/green] {strategy.name.title()} hooks passed: {len(results)} / {len(results)} "
423
- f"(async, {performance_gain: .1f} % faster)",
424
- )
425
- else:
426
- failed_count = sum(1 for r in results if r.status == "failed")
427
- error_count = sum(1 for r in results if r.status in ("timeout", "error"))
428
- self.console.print(
429
- f"[red]❌[/red] {strategy.name.title()} hooks failed: {failed_count} failed, {error_count} errors "
479
+ f"[green]✅[/ green] {strategy.name.title()} hooks passed: {len(results)} / {len(results)} "
430
480
  f"(async, {performance_gain: .1f} % faster)",
431
481
  )
@@ -95,7 +95,7 @@ class CachedHookExecutor:
95
95
  success = all(result.status == "passed" for result in results)
96
96
 
97
97
  self.logger.info(
98
- f"Cached strategy '{strategy.name}' completed in {total_time: .2f}s - "
98
+ f"Cached strategy '{strategy.name}' completed in {total_time: .2f}s-"
99
99
  f"Success: {success}, Cache hits: {cache_hits}, Cache misses: {cache_misses}",
100
100
  )
101
101
 
@@ -124,8 +124,8 @@ class CachedHookExecutor:
124
124
 
125
125
  def _strategy_affects_python_only(self, strategy: HookStrategy) -> bool:
126
126
  python_only_hooks = {
127
- "ruff - format",
128
- "ruff - check",
127
+ "ruff-format",
128
+ "ruff-check",
129
129
  "pyright",
130
130
  "bandit",
131
131
  "vulture",
@@ -140,17 +140,17 @@ class CachedHookExecutor:
140
140
 
141
141
  def _should_ignore_file(self, file_path: Path) -> bool:
142
142
  ignore_patterns = [
143
- ".git/",
144
- ".venv/",
145
- "__pycache__/",
146
- ".pytest_cache/",
143
+ ".git /",
144
+ ".venv /",
145
+ "__pycache__ /",
146
+ ".pytest_cache /",
147
147
  ".coverage",
148
- ".crackerjack_cache/",
149
- "node_modules/",
150
- ".tox/",
151
- "dist/",
152
- "build/",
153
- ".egg - info/",
148
+ ".crackerjack_cache /",
149
+ "node_modules /",
150
+ ".tox /",
151
+ "dist /",
152
+ "build /",
153
+ ".egg-info /",
154
154
  ]
155
155
 
156
156
  path_str = str(file_path)
@@ -188,7 +188,7 @@ class SmartCacheManager:
188
188
  hook_name: str,
189
189
  project_state: dict[str, t.Any],
190
190
  ) -> bool:
191
- external_hooks = {"detect - secrets"}
191
+ external_hooks = {"detect-secrets"}
192
192
  if hook_name in external_hooks:
193
193
  return False
194
194
 
@@ -10,6 +10,7 @@ from rich.console import Console
10
10
 
11
11
  from crackerjack.config.hooks import HookDefinition, HookStrategy, RetryPolicy
12
12
  from crackerjack.models.task import HookResult
13
+ from crackerjack.services.security_logger import get_security_logger
13
14
 
14
15
 
15
16
  @dataclass
@@ -169,22 +170,43 @@ class HookExecutor:
169
170
  def _run_hook_subprocess(
170
171
  self, hook: HookDefinition
171
172
  ) -> subprocess.CompletedProcess[str]:
172
- """Execute hook subprocess with clean environment."""
173
+ """Run hook subprocess with comprehensive security validation."""
174
+ # Get sanitized environment
173
175
  clean_env = self._get_clean_environment()
174
- return subprocess.run(
175
- hook.get_command(),
176
- check=False,
177
- cwd=self.pkg_path,
178
- text=True,
179
- timeout=hook.timeout,
180
- env=clean_env,
181
- capture_output=True,
182
- )
176
+
177
+ # Use secure subprocess execution
178
+ try:
179
+ # Pre-commit must run from repository root
180
+ # For crackerjack package structure, the repo root is pkg_path itself
181
+ repo_root = self.pkg_path
182
+ # Pre-commit has compatibility issues with secure subprocess
183
+ # Use direct subprocess execution for hooks
184
+ return subprocess.run(
185
+ hook.get_command(),
186
+ cwd=repo_root,
187
+ env=clean_env,
188
+ timeout=hook.timeout,
189
+ capture_output=True,
190
+ text=True,
191
+ check=False, # Don't raise on non-zero exit codes
192
+ )
193
+ except Exception as e:
194
+ # Log security issues but convert to subprocess-compatible result
195
+ security_logger = get_security_logger()
196
+ security_logger.log_subprocess_failure(
197
+ command=hook.get_command(),
198
+ exit_code=-1,
199
+ error_output=str(e),
200
+ )
201
+
202
+ # Return a failed CompletedProcess for consistency
203
+ return subprocess.CompletedProcess(
204
+ args=hook.get_command(), returncode=1, stdout="", stderr=str(e)
205
+ )
183
206
 
184
207
  def _display_hook_output_if_needed(
185
208
  self, result: subprocess.CompletedProcess[str]
186
209
  ) -> None:
187
- """Display hook output in verbose mode for failed hooks."""
188
210
  if result.returncode == 0 or not self.verbose:
189
211
  return
190
212
 
@@ -199,9 +221,18 @@ class HookExecutor:
199
221
  result: subprocess.CompletedProcess[str],
200
222
  duration: float,
201
223
  ) -> HookResult:
202
- """Create HookResult from successful subprocess execution."""
203
- status = "passed" if result.returncode == 0 else "failed"
204
- issues_found = self._extract_issues_from_process_output(result)
224
+ # Formatting hooks return 1 when they fix files, which is success
225
+ if hook.is_formatting and result.returncode == 1:
226
+ # Check if files were modified (successful formatting)
227
+ output_text = result.stdout + result.stderr
228
+ if "files were modified by this hook" in output_text:
229
+ status = "passed"
230
+ else:
231
+ status = "failed"
232
+ else:
233
+ status = "passed" if result.returncode == 0 else "failed"
234
+
235
+ issues_found = self._extract_issues_from_process_output(hook, result, status)
205
236
 
206
237
  return HookResult(
207
238
  id=hook.name,
@@ -214,37 +245,41 @@ class HookExecutor:
214
245
  )
215
246
 
216
247
  def _extract_issues_from_process_output(
217
- self, result: subprocess.CompletedProcess[str]
248
+ self,
249
+ hook: HookDefinition,
250
+ result: subprocess.CompletedProcess[str],
251
+ status: str,
218
252
  ) -> list[str]:
219
- """Extract specific issues from subprocess output for failed hooks."""
220
- if result.returncode == 0:
253
+ if status == "passed":
221
254
  return []
222
255
 
223
256
  error_output = (result.stdout + result.stderr).strip()
257
+
258
+ # For formatting hooks that successfully modified files, don't report as issues
259
+ if hook.is_formatting and "files were modified by this hook" in error_output:
260
+ return []
261
+
224
262
  if error_output:
225
263
  return [line.strip() for line in error_output.split("\n") if line.strip()]
226
264
 
227
- # Fallback to generic message if no output captured
228
265
  return [f"Hook failed with code {result.returncode}"]
229
266
 
230
267
  def _create_timeout_result(
231
268
  self, hook: HookDefinition, start_time: float
232
269
  ) -> HookResult:
233
- """Create HookResult for timeout scenarios."""
234
270
  duration = time.time() - start_time
235
271
  return HookResult(
236
272
  id=hook.name,
237
273
  name=hook.name,
238
274
  status="timeout",
239
275
  duration=duration,
240
- issues_found=[f"Hook timed out after {duration:.1f}s"],
276
+ issues_found=[f"Hook timed out after {duration: .1f}s"],
241
277
  stage=hook.stage.value,
242
278
  )
243
279
 
244
280
  def _create_error_result(
245
281
  self, hook: HookDefinition, start_time: float, error: Exception
246
282
  ) -> HookResult:
247
- """Create HookResult for general exception scenarios."""
248
283
  duration = time.time() - start_time
249
284
  return HookResult(
250
285
  id=hook.name,
@@ -271,14 +306,11 @@ class HookExecutor:
271
306
  def _display_hook_result(self, result: HookResult) -> None:
272
307
  status_icon = "✅" if result.status == "passed" else "❌"
273
308
 
274
- # Create dot-filled line like classic pre-commit format
275
- max_width = 70 # Leave room for terminal margins
309
+ max_width = 70
276
310
 
277
311
  if len(result.name) > max_width:
278
- # Truncate long names
279
312
  line = result.name[: max_width - 3] + "..."
280
313
  else:
281
- # Fill with dots to reach max width
282
314
  dots_needed = max_width - len(result.name)
283
315
  line = result.name + ("." * dots_needed)
284
316
 
@@ -344,6 +376,13 @@ class HookExecutor:
344
376
  return updated_results
345
377
 
346
378
  def _get_clean_environment(self) -> dict[str, str]:
379
+ """
380
+ Get a sanitized environment for hook execution.
381
+
382
+ This method now delegates to the secure subprocess utilities
383
+ for comprehensive environment sanitization with security logging.
384
+ """
385
+ # Create base environment with essential variables
347
386
  clean_env = {
348
387
  "HOME": os.environ.get("HOME", ""),
349
388
  "USER": os.environ.get("USER", ""),
@@ -353,12 +392,14 @@ class HookExecutor:
353
392
  "TERM": os.environ.get("TERM", "xterm-256color"),
354
393
  }
355
394
 
395
+ # Handle PATH sanitization with venv filtering
356
396
  system_path = os.environ.get("PATH", "")
357
397
  if system_path:
358
398
  venv_bin = str(Path(self.pkg_path) / ".venv" / "bin")
359
399
  path_parts = [p for p in system_path.split(":") if p != venv_bin]
360
400
  clean_env["PATH"] = ":".join(path_parts)
361
401
 
402
+ # Define Python-specific variables to exclude
362
403
  python_vars_to_exclude = {
363
404
  "VIRTUAL_ENV",
364
405
  "PYTHONPATH",
@@ -366,12 +407,41 @@ class HookExecutor:
366
407
  "PIP_CONFIG_FILE",
367
408
  "PYTHONHOME",
368
409
  "CONDA_DEFAULT_ENV",
410
+ "PIPENV_ACTIVE",
411
+ "POETRY_ACTIVE",
369
412
  }
370
413
 
414
+ # Add other safe environment variables
415
+ security_logger = get_security_logger()
416
+ original_count = len(os.environ)
417
+ filtered_count = 0
418
+
371
419
  for key, value in os.environ.items():
372
420
  if key not in python_vars_to_exclude and key not in clean_env:
373
- if not key.startswith(("PYTHON", "PIP_", "CONDA_", "VIRTUAL_")):
374
- clean_env[key] = value
421
+ # Additional security filtering
422
+ if not key.startswith(
423
+ ("PYTHON", "PIP_", "CONDA_", "VIRTUAL_", "__PYVENV")
424
+ ):
425
+ # Check for dangerous environment variables
426
+ if key not in {"LD_PRELOAD", "DYLD_INSERT_LIBRARIES", "IFS", "PS4"}:
427
+ clean_env[key] = value
428
+ else:
429
+ filtered_count += 1
430
+ security_logger.log_environment_variable_filtered(
431
+ variable_name=key,
432
+ reason="dangerous environment variable",
433
+ value_preview=(value[:50] if value else "")[:50],
434
+ )
435
+ else:
436
+ filtered_count += 1
437
+
438
+ # Log environment sanitization if significant filtering occurred
439
+ if filtered_count > 5: # Only log if substantial filtering
440
+ security_logger.log_subprocess_environment_sanitized(
441
+ original_count=original_count,
442
+ sanitized_count=len(clean_env),
443
+ filtered_vars=[], # Don't expose all filtered vars for performance
444
+ )
375
445
 
376
446
  return clean_env
377
447
 
@@ -381,13 +451,6 @@ class HookExecutor:
381
451
  results: list[HookResult],
382
452
  success: bool,
383
453
  ) -> None:
384
- if success:
385
- self.console.print(
386
- f"[green]✅[/green] {strategy.name.title()} hooks passed: {len(results)} / {len(results)}",
387
- )
388
- else:
389
- failed_count = sum(1 for r in results if r.status == "failed")
390
- error_count = sum(1 for r in results if r.status in ("timeout", "error"))
391
- self.console.print(
392
- f"[red]❌[/red] {strategy.name.title()} hooks failed: {failed_count} failed, {error_count} errors",
393
- )
454
+ # Summary is handled by PhaseCoordinator to avoid duplicate messages
455
+ # Individual hook results are already displayed above
456
+ pass