claude-dev-env 1.25.2 → 1.26.1

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.
Files changed (106) hide show
  1. package/CLAUDE.md +6 -0
  2. package/agents/clean-coder.md +1 -1
  3. package/docs/CODE_RULES.md +3 -1
  4. package/hooks/HOOK_SPECS_PROMPT_WORKFLOW.md +54 -0
  5. package/hooks/blocking/{code-rules-enforcer.py → code_rules_enforcer.py} +154 -5
  6. package/hooks/blocking/test_code_rules_enforcer.py +61 -0
  7. package/hooks/blocking/test_code_rules_enforcer_any_type_ignore.py +2 -2
  8. package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +2 -2
  9. package/hooks/blocking/test_code_rules_enforcer_conftest_anchor.py +1 -1
  10. package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +2 -2
  11. package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +183 -0
  12. package/hooks/blocking/test_code_rules_enforcer_fstring_scan.py +4 -4
  13. package/hooks/blocking/test_code_rules_enforcer_logger_fstring.py +1 -1
  14. package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
  15. package/hooks/blocking/test_code_rules_enforcer_magic_string_masking.py +104 -0
  16. package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +2 -2
  17. package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +2 -2
  18. package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +1 -1
  19. package/hooks/blocking/test_destructive_command_blocker.py +1 -1
  20. package/hooks/blocking/test_gh_body_arg_blocker.py +1 -1
  21. package/hooks/blocking/test_pr_description_enforcer.py +8 -8
  22. package/hooks/blocking/test_tdd_enforcer.py +1 -1
  23. package/hooks/github-action/pre-push-review.yml +27 -0
  24. package/hooks/hooks.json +28 -28
  25. package/hooks/lifecycle/{config-change-guard.py → config_change_guard.py} +26 -12
  26. package/hooks/lifecycle/test_config_change_guard.py +3 -3
  27. package/hooks/notification/{attention-needed-notify.py → attention_needed_notify.py} +7 -0
  28. package/hooks/notification/{claude-notification-handler.py → claude_notification_handler.py} +8 -0
  29. package/hooks/notification/notification_utils.py +56 -0
  30. package/hooks/notification/subagent_complete_notify.py +381 -0
  31. package/hooks/notification/test_attention_needed_notify.py +47 -0
  32. package/hooks/notification/test_claude_notification_handler.py +54 -0
  33. package/hooks/notification/test_notification_utils.py +45 -0
  34. package/hooks/notification/test_subagent_complete_notify.py +79 -0
  35. package/hooks/validators/README.md +5 -1
  36. package/hooks/validators/abbreviation_checks.py +1 -1
  37. package/hooks/validators/code_quality_checks.py +1 -1
  38. package/hooks/validators/config.py +5 -0
  39. package/hooks/validators/conftest.py +10 -0
  40. package/hooks/validators/exempt_paths.py +1 -1
  41. package/hooks/validators/git_checks.py +80 -0
  42. package/hooks/validators/magic_value_checks.py +2 -2
  43. package/hooks/validators/pr_reference_checks.py +1 -1
  44. package/hooks/validators/python_antipattern_checks.py +1 -1
  45. package/hooks/validators/run_all_validators.py +53 -105
  46. package/hooks/validators/security_checks.py +1 -1
  47. package/hooks/validators/test_abbreviation_checks.py +2 -2
  48. package/hooks/validators/test_code_quality_checks.py +2 -2
  49. package/hooks/validators/test_file_structure_checks.py +1 -1
  50. package/hooks/validators/test_git_checks.py +79 -13
  51. package/hooks/validators/test_health_check.py +1 -1
  52. package/hooks/validators/test_magic_value_checks.py +2 -2
  53. package/hooks/validators/test_mypy_integration.py +1 -1
  54. package/hooks/validators/test_output_formatter.py +3 -1
  55. package/hooks/validators/test_pr_reference_checks.py +2 -2
  56. package/hooks/validators/test_python_antipattern_checks.py +2 -2
  57. package/hooks/validators/test_python_style_checks.py +2 -4
  58. package/hooks/validators/test_react_checks.py +1 -1
  59. package/hooks/validators/test_ruff_integration.py +1 -1
  60. package/hooks/validators/test_run_all_validators.py +75 -43
  61. package/hooks/validators/test_run_all_validators_integration.py +14 -37
  62. package/hooks/validators/test_security_checks.py +2 -2
  63. package/hooks/validators/test_test_safety_checks.py +1 -1
  64. package/hooks/validators/test_todo_checks.py +2 -2
  65. package/hooks/validators/test_type_safety_checks.py +2 -2
  66. package/hooks/validators/test_useless_test_checks.py +2 -2
  67. package/hooks/validators/test_validator_base.py +1 -1
  68. package/hooks/validators/test_verify_paths.py +2 -4
  69. package/hooks/validators/todo_checks.py +1 -1
  70. package/hooks/validators/type_safety_checks.py +1 -1
  71. package/hooks/validators/useless_test_checks.py +1 -1
  72. package/package.json +1 -1
  73. package/rules/file-global-constants.md +71 -0
  74. package/rules/gh-body-file.md +1 -1
  75. package/rules/prompt-workflow-context-controls.md +48 -0
  76. package/scripts/sync_to_cursor/rules.py +2 -2
  77. package/scripts/tests/test_sync_to_cursor.py +2 -2
  78. package/skills/bugteam/CONSTRAINTS.md +37 -0
  79. package/skills/bugteam/EXAMPLES.md +64 -0
  80. package/skills/bugteam/PROMPTS.md +175 -0
  81. package/skills/bugteam/SKILL.md +204 -295
  82. package/skills/bugteam/SKILL_EVALS.md +346 -0
  83. package/skills/bugteam/scripts/README.md +37 -0
  84. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +334 -0
  85. package/skills/bugteam/scripts/bugteam_preflight.py +135 -0
  86. package/skills/rule-audit/SKILL.md +4 -4
  87. /package/hooks/advisory/{migration-safety-advisor.py → migration_safety_advisor.py} +0 -0
  88. /package/hooks/advisory/{refactor-guard.py → refactor_guard.py} +0 -0
  89. /package/hooks/blocking/{block-main-commit.py → block_main_commit.py} +0 -0
  90. /package/hooks/blocking/{content-search-to-zoekt-redirector.py → content_search_to_zoekt_redirector.py} +0 -0
  91. /package/hooks/blocking/{destructive-command-blocker.py → destructive_command_blocker.py} +0 -0
  92. /package/hooks/blocking/{gh-body-arg-blocker.py → gh_body_arg_blocker.py} +0 -0
  93. /package/hooks/blocking/{hedging-language-blocker.py → hedging_language_blocker.py} +0 -0
  94. /package/hooks/blocking/{pr-description-enforcer.py → pr_description_enforcer.py} +0 -0
  95. /package/hooks/blocking/{sensitive-file-protector.py → sensitive_file_protector.py} +0 -0
  96. /package/hooks/blocking/{tdd-enforcer.py → tdd_enforcer.py} +0 -0
  97. /package/hooks/blocking/{test-preflight-check.py → test_preflight_check.py} +0 -0
  98. /package/hooks/blocking/{write-existing-file-blocker.py → write_existing_file_blocker.py} +0 -0
  99. /package/hooks/git-hooks/{post-commit.py → post_commit.py} +0 -0
  100. /package/hooks/lifecycle/{session-end-cleanup.py → session_end_cleanup.py} +0 -0
  101. /package/hooks/{rewrite-plugin-paths.py → rewrite_plugin_paths.py} +0 -0
  102. /package/hooks/session/{plugin-data-dir-cleanup.py → plugin_data_dir_cleanup.py} +0 -0
  103. /package/hooks/validation/{hook-format-validator.py → hook_format_validator.py} +0 -0
  104. /package/hooks/workflow/{auto-formatter.py → auto_formatter.py} +0 -0
  105. /package/hooks/workflow/{investigation-tracker-reset.py → investigation_tracker_reset.py} +0 -0
  106. /package/scripts/{sync-to-cursor.py → sync_to_cursor.py} +0 -0
@@ -0,0 +1,79 @@
1
+ """Unit tests for subagent-complete-notify Discord wiring."""
2
+
3
+ import importlib.util
4
+ import pathlib
5
+ import types
6
+ from unittest.mock import patch
7
+
8
+ HOOK_DIRECTORY = pathlib.Path(__file__).parent
9
+ MODULE_PATH = HOOK_DIRECTORY / "subagent_complete_notify.py"
10
+
11
+ FIXTURE_ACTIVITY_SECRET_ID = "fixture-activity-id-0003"
12
+ FIXTURE_TASK_DESCRIPTION = "subagent finished research task"
13
+ FIXTURE_PROJECT_NAME = "fixture-project"
14
+ NON_WINDOWS_NON_WSL_PLATFORM = "Darwin"
15
+
16
+
17
+ def load_hook_with_environment(
18
+ environment_overrides: dict[str, str],
19
+ ) -> types.ModuleType:
20
+ module_specification = importlib.util.spec_from_file_location(
21
+ "subagent_complete_notify_under_test",
22
+ MODULE_PATH,
23
+ )
24
+ assert module_specification is not None
25
+ assert module_specification.loader is not None
26
+ module_under_test = importlib.util.module_from_spec(module_specification)
27
+ with patch.dict("os.environ", environment_overrides, clear=False):
28
+ module_specification.loader.exec_module(module_under_test)
29
+ return module_under_test
30
+
31
+
32
+ def test_main_forwards_activity_secret_id_to_notify_discord() -> None:
33
+ module_under_test = load_hook_with_environment(
34
+ {"BWS_DISCORD_ACTIVITY_SECRET_ID": FIXTURE_ACTIVITY_SECRET_ID}
35
+ )
36
+ with (
37
+ patch.object(
38
+ module_under_test,
39
+ "get_task_info_from_stdin",
40
+ return_value=FIXTURE_TASK_DESCRIPTION,
41
+ ),
42
+ patch.object(
43
+ module_under_test, "get_project_name", return_value=FIXTURE_PROJECT_NAME
44
+ ),
45
+ patch.object(module_under_test, "notify_ntfy"),
46
+ patch.object(module_under_test, "notify_discord") as discord_spy,
47
+ patch.object(module_under_test, "is_wsl", return_value=False),
48
+ patch.object(module_under_test, "platform") as platform_stub,
49
+ ):
50
+ platform_stub.system.return_value = NON_WINDOWS_NON_WSL_PLATFORM
51
+ module_under_test.main()
52
+ assert discord_spy.call_count == 1
53
+ call_kwargs = discord_spy.call_args.kwargs
54
+ assert call_kwargs["webhook_secret_id"] == FIXTURE_ACTIVITY_SECRET_ID
55
+ assert call_kwargs["title"] == FIXTURE_PROJECT_NAME
56
+ assert call_kwargs["message"] == FIXTURE_TASK_DESCRIPTION
57
+
58
+
59
+ def test_notify_ntfy_skips_when_topic_unset() -> None:
60
+ module_under_test = load_hook_with_environment({"CLAUDE_NTFY_TOPIC": ""})
61
+ with patch.object(module_under_test.subprocess, "Popen") as popen_spy:
62
+ module_under_test.notify_ntfy(title="t", message="m")
63
+ assert popen_spy.call_count == 0
64
+
65
+
66
+ def test_main_skips_notify_discord_when_task_description_is_empty() -> None:
67
+ module_under_test = load_hook_with_environment(
68
+ {"BWS_DISCORD_ACTIVITY_SECRET_ID": FIXTURE_ACTIVITY_SECRET_ID}
69
+ )
70
+ with (
71
+ patch.object(module_under_test, "get_task_info_from_stdin", return_value=""),
72
+ patch.object(
73
+ module_under_test, "get_project_name", return_value=FIXTURE_PROJECT_NAME
74
+ ),
75
+ patch.object(module_under_test, "notify_ntfy"),
76
+ patch.object(module_under_test, "notify_discord") as discord_spy,
77
+ ):
78
+ module_under_test.main()
79
+ assert discord_spy.call_count == 0
@@ -119,7 +119,11 @@ repos:
119
119
  hooks:
120
120
  - id: python-style-checks
121
121
  name: Python Style Checks
122
- entry: python hooks/validators/python_style_checks.py
122
+ entry: python packages/claude-dev-env/hooks/validators/python_style_checks.py
123
+ args: []
124
+ pass_filenames: true
125
+ # Invokes the script directly via its ``__main__`` block so the
126
+ # ``validators`` package qualifier does not need PYTHONPATH setup.
123
127
  language: system
124
128
  types: [python]
125
129
  ```
@@ -13,7 +13,7 @@ import sys
13
13
  from pathlib import Path
14
14
  from typing import List, Set
15
15
 
16
- from validator_base import Violation
16
+ from .validator_base import Violation
17
17
 
18
18
 
19
19
  ALLOWED_SINGLE_LETTERS: Set[str] = frozenset({"i", "j", "k", "_"})
@@ -11,7 +11,7 @@ import sys
11
11
  from pathlib import Path
12
12
  from typing import List
13
13
 
14
- from validator_base import Violation
14
+ from .validator_base import Violation
15
15
 
16
16
 
17
17
  MAX_FUNCTION_LINES = 30
@@ -0,0 +1,5 @@
1
+ """Shared constants for the validators package."""
2
+
3
+ # pragma: no-tdd-gate
4
+
5
+ DEFAULT_BASE_BRANCH_WHEN_UNKNOWN = "main"
@@ -0,0 +1,10 @@
1
+ """Pytest fixture module ensuring validators directory is importable regardless of invocation cwd."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+
7
+ VALIDATORS_DIRECTORY = Path(__file__).resolve().parent
8
+
9
+ if str(VALIDATORS_DIRECTORY) not in sys.path:
10
+ sys.path.insert(0, str(VALIDATORS_DIRECTORY))
@@ -2,7 +2,7 @@
2
2
 
3
3
  Single source of truth for CONFIG / TEST / HOOK-INFRASTRUCTURE /
4
4
  WORKFLOW-REGISTRY / MIGRATION path pattern sets. Both Pre-Write
5
- (``code-rules-enforcer.py``) and pre-push (``magic_value_checks.py``)
5
+ (``code_rules_enforcer.py``) and pre-push (``magic_value_checks.py``)
6
6
  scanners must short-circuit on the same file categories; drift between
7
7
  the two produced the "inconsistent verdicts" bug this module prevents.
8
8
 
@@ -6,6 +6,8 @@ import sys
6
6
  from dataclasses import dataclass
7
7
  from typing import List
8
8
 
9
+ from .config import DEFAULT_BASE_BRANCH_WHEN_UNKNOWN
10
+
9
11
 
10
12
  SUBPROCESS_TIMEOUT_SECONDS = 30
11
13
 
@@ -33,6 +35,83 @@ def get_current_branch() -> str:
33
35
  return ""
34
36
 
35
37
 
38
+ def check_single_commit_when_pr_exists() -> List[Violation]:
39
+ """
40
+ Check that a PR branch has exactly 1 commit ahead of its base.
41
+
42
+ Returns empty list if:
43
+ - No PR exists for current branch
44
+ - gh CLI or git is not available
45
+ - gh/git times out
46
+ - Branch is exactly 1 commit ahead of base
47
+ - git rev-list output is non-numeric
48
+
49
+ Returns violation if:
50
+ - Branch is 0 commits ahead of base
51
+ - Branch is more than 1 commit ahead of base
52
+ """
53
+ branch = get_current_branch()
54
+ if not branch:
55
+ return []
56
+
57
+ try:
58
+ pr_info = subprocess.run(
59
+ ["gh", "pr", "list", "--head", branch, "--json", "baseRefName,number"],
60
+ capture_output=True,
61
+ text=True,
62
+ check=True,
63
+ timeout=SUBPROCESS_TIMEOUT_SECONDS,
64
+ )
65
+ except FileNotFoundError:
66
+ return []
67
+ except subprocess.CalledProcessError:
68
+ return []
69
+ except subprocess.TimeoutExpired:
70
+ return []
71
+
72
+ try:
73
+ pr_data = json.loads(pr_info.stdout)
74
+ except json.JSONDecodeError:
75
+ return []
76
+
77
+ if not pr_data:
78
+ return []
79
+
80
+ base_branch = pr_data[0].get("baseRefName", DEFAULT_BASE_BRANCH_WHEN_UNKNOWN)
81
+
82
+ try:
83
+ rev_list = subprocess.run(
84
+ ["git", "rev-list", "--count", f"{base_branch}..HEAD"],
85
+ capture_output=True,
86
+ text=True,
87
+ check=True,
88
+ timeout=SUBPROCESS_TIMEOUT_SECONDS,
89
+ )
90
+ except FileNotFoundError:
91
+ return []
92
+ except subprocess.CalledProcessError:
93
+ return []
94
+ except subprocess.TimeoutExpired:
95
+ return []
96
+
97
+ commit_count_text = rev_list.stdout.strip()
98
+ try:
99
+ commit_count = int(commit_count_text)
100
+ except ValueError:
101
+ return []
102
+
103
+ if commit_count == 1:
104
+ return []
105
+
106
+ return [
107
+ Violation(
108
+ file="",
109
+ line=0,
110
+ message=f"Branch must have exactly 1 commit ahead of {base_branch}, found {commit_count} commits",
111
+ )
112
+ ]
113
+
114
+
36
115
  def check_draft_pr_state() -> List[Violation]:
37
116
  """
38
117
  Check that PR is in draft state.
@@ -93,6 +172,7 @@ def main() -> None:
93
172
  """Run all git checks and exit with appropriate code."""
94
173
  violations: List[Violation] = []
95
174
 
175
+ violations.extend(check_single_commit_when_pr_exists())
96
176
  violations.extend(check_draft_pr_state())
97
177
 
98
178
  if violations:
@@ -12,11 +12,11 @@ import sys
12
12
  from pathlib import Path
13
13
  from typing import Dict, FrozenSet, List, Set, Tuple, Type
14
14
 
15
- from exempt_paths import (
15
+ from .exempt_paths import (
16
16
  is_config_file,
17
17
  is_test_file,
18
18
  )
19
- from validator_base import Violation
19
+ from .validator_base import Violation
20
20
 
21
21
 
22
22
  ALLOWED_NUMBERS: FrozenSet[int] = frozenset({-1, 0, 1})
@@ -9,7 +9,7 @@ import sys
9
9
  from pathlib import Path
10
10
  from typing import List
11
11
 
12
- from validator_base import Violation
12
+ from .validator_base import Violation
13
13
 
14
14
 
15
15
  PR_PATTERN = re.compile(r"#.*\bPR\s*#?\d+", re.IGNORECASE)
@@ -11,7 +11,7 @@ import sys
11
11
  from pathlib import Path
12
12
  from typing import List
13
13
 
14
- from validator_base import Violation
14
+ from .validator_base import Violation
15
15
 
16
16
 
17
17
  def check_mutable_default_args(tree: ast.AST, filename: str) -> List[Violation]:
@@ -3,6 +3,7 @@
3
3
  This script orchestrates all automated validators and produces a unified report.
4
4
  Exit code 0 = all checks pass, 1 = violations found.
5
5
  """
6
+ # pragma: no-tdd-gate
6
7
 
7
8
  import argparse
8
9
  import subprocess
@@ -13,13 +14,31 @@ from datetime import datetime
13
14
  from pathlib import Path
14
15
  from typing import Any, Callable, Dict, List, Optional, Tuple
15
16
 
16
- from health_check import get_validator_version
17
- from mypy_integration import check_mypy_available, run_mypy_check
18
- from output_formatter import OutputFormatter, OutputMode, ValidatorResultDict
19
- from ruff_integration import check_ruff_available, run_ruff_check
17
+ from .health_check import get_system_health, get_validator_version, print_health_report
18
+ from .mypy_integration import check_mypy_available, run_mypy_check
19
+ from .output_formatter import OutputFormatter, OutputMode, ValidatorResultDict
20
+ from .python_style_checks import fix_file
21
+ from .ruff_integration import check_ruff_available, run_ruff_check
20
22
 
21
23
 
22
24
  VALIDATORS_DIR = Path(__file__).parent
25
+ hooks_dir = VALIDATORS_DIR.parent
26
+ package_name = VALIDATORS_DIR.name
27
+
28
+
29
+ def invoke_validator_module(module_stem: str, forwarded_file_paths: List[str]) -> subprocess.CompletedProcess[str]: # pragma: no-tdd-gate
30
+ """Run a sibling validator as ``python -m validators.<module_stem>``.
31
+
32
+ The subprocess is launched with ``cwd`` set to the hooks directory so the
33
+ ``validators`` package qualifier resolves without requiring PYTHONPATH.
34
+ """
35
+ qualified_module = ".".join([package_name, module_stem])
36
+ return subprocess.run(
37
+ [sys.executable, "-m", qualified_module, *forwarded_file_paths],
38
+ capture_output=True,
39
+ text=True,
40
+ cwd=str(hooks_dir),
41
+ )
23
42
 
24
43
 
25
44
  @dataclass(frozen=True)
@@ -169,18 +188,13 @@ def run_python_style_checks(files: List[Path]) -> ValidatorResult:
169
188
  output="No Python files to check",
170
189
  )
171
190
 
172
- result = subprocess.run(
173
- [sys.executable, str(VALIDATORS_DIR / "python_style_checks.py")]
174
- + [str(f) for f in py_files],
175
- capture_output=True,
176
- text=True,
177
- )
191
+ result = invoke_validator_module("python_style_checks", [str(f) for f in py_files])
178
192
 
179
193
  return ValidatorResult(
180
194
  name="Python Style",
181
195
  checks="1,2,3,4",
182
196
  passed=result.returncode == 0,
183
- output=result.stdout or "All checks passed",
197
+ output=result.stdout or result.stderr or "All checks passed",
184
198
  )
185
199
 
186
200
 
@@ -195,18 +209,13 @@ def run_test_safety_checks(files: List[Path]) -> ValidatorResult:
195
209
  output="No test files to check",
196
210
  )
197
211
 
198
- result = subprocess.run(
199
- [sys.executable, str(VALIDATORS_DIR / "test_safety_checks.py")]
200
- + [str(f) for f in test_files],
201
- capture_output=True,
202
- text=True,
203
- )
212
+ result = invoke_validator_module("test_safety_checks", [str(f) for f in test_files])
204
213
 
205
214
  return ValidatorResult(
206
215
  name="Test Safety",
207
216
  checks="11,21",
208
217
  passed=result.returncode == 0,
209
- output=result.stdout or "All checks passed",
218
+ output=result.stdout or result.stderr or "All checks passed",
210
219
  )
211
220
 
212
221
 
@@ -235,17 +244,13 @@ def run_file_structure_checks(project_root: Optional[Path] = None) -> ValidatorR
235
244
  output="Not in a git repository - skipping",
236
245
  )
237
246
 
238
- result = subprocess.run(
239
- [sys.executable, str(VALIDATORS_DIR / "file_structure_checks.py"), str(project_root)],
240
- capture_output=True,
241
- text=True,
242
- )
247
+ result = invoke_validator_module("file_structure_checks", [str(project_root)])
243
248
 
244
249
  return ValidatorResult(
245
250
  name="File Structure",
246
251
  checks="14,15",
247
252
  passed=result.returncode == 0,
248
- output=result.stdout or "All checks passed",
253
+ output=result.stdout or result.stderr or "All checks passed",
249
254
  )
250
255
 
251
256
 
@@ -260,39 +265,30 @@ def run_react_checks(files: List[Path]) -> ValidatorResult:
260
265
  output="No React files to check",
261
266
  )
262
267
 
263
- result = subprocess.run(
264
- [sys.executable, str(VALIDATORS_DIR / "react_checks.py")]
265
- + [str(f) for f in react_files],
266
- capture_output=True,
267
- text=True,
268
- )
268
+ result = invoke_validator_module("react_checks", [str(f) for f in react_files])
269
269
 
270
270
  return ValidatorResult(
271
271
  name="React",
272
272
  checks="17",
273
273
  passed=result.returncode == 0,
274
- output=result.stdout or "All checks passed",
274
+ output=result.stdout or result.stderr or "All checks passed",
275
275
  )
276
276
 
277
277
 
278
278
  def run_git_checks() -> ValidatorResult:
279
279
  """Run git/GitHub checks."""
280
- result = subprocess.run(
281
- [sys.executable, str(VALIDATORS_DIR / "git_checks.py")],
282
- capture_output=True,
283
- text=True,
284
- )
280
+ result = invoke_validator_module("git_checks", [])
285
281
 
286
282
  return ValidatorResult(
287
283
  name="Git/PR Workflow",
288
284
  checks="23,24",
289
285
  passed=result.returncode == 0,
290
- output=result.stdout or "All checks passed",
286
+ output=result.stdout or result.stderr or "All checks passed",
291
287
  )
292
288
 
293
289
 
294
290
  def run_comment_checks(files: List[Path]) -> ValidatorResult:
295
- """Comment preservation is enforced by code-rules-enforcer hook.
291
+ """Comment preservation is enforced by code_rules_enforcer hook.
296
292
 
297
293
  The hook compares old vs new content to block NEW comments and
298
294
  block DELETION of existing comments. This standalone validator
@@ -303,7 +299,7 @@ def run_comment_checks(files: List[Path]) -> ValidatorResult:
303
299
  name="No Comments",
304
300
  checks="26",
305
301
  passed=True,
306
- output="Handled by code-rules-enforcer hook (old vs new comparison)",
302
+ output="Handled by code_rules_enforcer hook (old vs new comparison)",
307
303
  )
308
304
 
309
305
 
@@ -358,18 +354,13 @@ def run_abbreviation_checks(files: List[Path]) -> ValidatorResult:
358
354
  output="No Python files to check",
359
355
  )
360
356
 
361
- result = subprocess.run(
362
- [sys.executable, str(VALIDATORS_DIR / "abbreviation_checks.py")]
363
- + [str(f) for f in py_files],
364
- capture_output=True,
365
- text=True,
366
- )
357
+ result = invoke_validator_module("abbreviation_checks", [str(f) for f in py_files])
367
358
 
368
359
  return ValidatorResult(
369
360
  name="Abbreviations",
370
361
  checks="5",
371
362
  passed=result.returncode == 0,
372
- output=result.stdout or "All checks passed",
363
+ output=result.stdout or result.stderr or "All checks passed",
373
364
  )
374
365
 
375
366
 
@@ -384,18 +375,13 @@ def run_pr_reference_checks(files: List[Path]) -> ValidatorResult:
384
375
  output="No code files to check",
385
376
  )
386
377
 
387
- result = subprocess.run(
388
- [sys.executable, str(VALIDATORS_DIR / "pr_reference_checks.py")]
389
- + [str(f) for f in code_files],
390
- capture_output=True,
391
- text=True,
392
- )
378
+ result = invoke_validator_module("pr_reference_checks", [str(f) for f in code_files])
393
379
 
394
380
  return ValidatorResult(
395
381
  name="PR References",
396
382
  checks="6",
397
383
  passed=result.returncode == 0,
398
- output=result.stdout or "All checks passed",
384
+ output=result.stdout or result.stderr or "All checks passed",
399
385
  )
400
386
 
401
387
 
@@ -410,18 +396,13 @@ def run_magic_value_checks(files: List[Path]) -> ValidatorResult:
410
396
  output="No Python files to check",
411
397
  )
412
398
 
413
- result = subprocess.run(
414
- [sys.executable, str(VALIDATORS_DIR / "magic_value_checks.py")]
415
- + [str(f) for f in py_files],
416
- capture_output=True,
417
- text=True,
418
- )
399
+ result = invoke_validator_module("magic_value_checks", [str(f) for f in py_files])
419
400
 
420
401
  return ValidatorResult(
421
402
  name="Magic Values",
422
403
  checks="7",
423
404
  passed=result.returncode == 0,
424
- output=result.stdout or "All checks passed",
405
+ output=result.stdout or result.stderr or "All checks passed",
425
406
  )
426
407
 
427
408
 
@@ -436,18 +417,13 @@ def run_useless_test_checks(files: List[Path]) -> ValidatorResult:
436
417
  output="No test files to check",
437
418
  )
438
419
 
439
- result = subprocess.run(
440
- [sys.executable, str(VALIDATORS_DIR / "useless_test_checks.py")]
441
- + [str(f) for f in test_files],
442
- capture_output=True,
443
- text=True,
444
- )
420
+ result = invoke_validator_module("useless_test_checks", [str(f) for f in test_files])
445
421
 
446
422
  return ValidatorResult(
447
423
  name="Useless Tests",
448
424
  checks="12",
449
425
  passed=result.returncode == 0,
450
- output=result.stdout or "All checks passed",
426
+ output=result.stdout or result.stderr or "All checks passed",
451
427
  )
452
428
 
453
429
 
@@ -462,18 +438,13 @@ def run_security_checks(files: List[Path]) -> ValidatorResult:
462
438
  output="No Python files to check",
463
439
  )
464
440
 
465
- result = subprocess.run(
466
- [sys.executable, str(VALIDATORS_DIR / "security_checks.py")]
467
- + [str(f) for f in py_files],
468
- capture_output=True,
469
- text=True,
470
- )
441
+ result = invoke_validator_module("security_checks", [str(f) for f in py_files])
471
442
 
472
443
  return ValidatorResult(
473
444
  name="Security",
474
445
  checks="27,28,29",
475
446
  passed=result.returncode == 0,
476
- output=result.stdout or "All checks passed",
447
+ output=result.stdout or result.stderr or "All checks passed",
477
448
  )
478
449
 
479
450
 
@@ -488,18 +459,13 @@ def run_code_quality_checks(files: List[Path]) -> ValidatorResult:
488
459
  output="No Python files to check",
489
460
  )
490
461
 
491
- result = subprocess.run(
492
- [sys.executable, str(VALIDATORS_DIR / "code_quality_checks.py")]
493
- + [str(f) for f in py_files],
494
- capture_output=True,
495
- text=True,
496
- )
462
+ result = invoke_validator_module("code_quality_checks", [str(f) for f in py_files])
497
463
 
498
464
  return ValidatorResult(
499
465
  name="Code Quality",
500
466
  checks="30,31,32",
501
467
  passed=result.returncode == 0,
502
- output=result.stdout or "All checks passed",
468
+ output=result.stdout or result.stderr or "All checks passed",
503
469
  )
504
470
 
505
471
 
@@ -514,18 +480,13 @@ def run_python_antipattern_checks(files: List[Path]) -> ValidatorResult:
514
480
  output="No Python files to check",
515
481
  )
516
482
 
517
- result = subprocess.run(
518
- [sys.executable, str(VALIDATORS_DIR / "python_antipattern_checks.py")]
519
- + [str(f) for f in py_files],
520
- capture_output=True,
521
- text=True,
522
- )
483
+ result = invoke_validator_module("python_antipattern_checks", [str(f) for f in py_files])
523
484
 
524
485
  return ValidatorResult(
525
486
  name="Python Anti-patterns",
526
487
  checks="33,34,35",
527
488
  passed=result.returncode == 0,
528
- output=result.stdout or "All checks passed",
489
+ output=result.stdout or result.stderr or "All checks passed",
529
490
  )
530
491
 
531
492
 
@@ -540,18 +501,13 @@ def run_todo_checks(files: List[Path]) -> ValidatorResult:
540
501
  output="No Python files to check",
541
502
  )
542
503
 
543
- result = subprocess.run(
544
- [sys.executable, str(VALIDATORS_DIR / "todo_checks.py")]
545
- + [str(f) for f in py_files],
546
- capture_output=True,
547
- text=True,
548
- )
504
+ result = invoke_validator_module("todo_checks", [str(f) for f in py_files])
549
505
 
550
506
  return ValidatorResult(
551
507
  name="TODO Tracking",
552
508
  checks="36",
553
509
  passed=result.returncode == 0,
554
- output=result.stdout or "All checks passed",
510
+ output=result.stdout or result.stderr or "All checks passed",
555
511
  )
556
512
 
557
513
 
@@ -566,18 +522,13 @@ def run_type_safety_checks(files: List[Path]) -> ValidatorResult:
566
522
  output="No Python files to check",
567
523
  )
568
524
 
569
- result = subprocess.run(
570
- [sys.executable, str(VALIDATORS_DIR / "type_safety_checks.py")]
571
- + [str(f) for f in py_files],
572
- capture_output=True,
573
- text=True,
574
- )
525
+ result = invoke_validator_module("type_safety_checks", [str(f) for f in py_files])
575
526
 
576
527
  return ValidatorResult(
577
528
  name="Type Safety",
578
529
  checks="39,40",
579
530
  passed=result.returncode == 0,
580
- output=result.stdout or "All checks passed",
531
+ output=result.stdout or result.stderr or "All checks passed",
581
532
  )
582
533
 
583
534
 
@@ -590,8 +541,6 @@ def fix_python_style(files: List[Path]) -> List[str]:
590
541
  Returns:
591
542
  List of files that were fixed
592
543
  """
593
- from python_style_checks import fix_file
594
-
595
544
  fixed_files: List[str] = []
596
545
  py_files = [f for f in files if f.suffix == ".py"]
597
546
 
@@ -657,7 +606,6 @@ def main() -> int:
657
606
  args = parser.parse_args()
658
607
 
659
608
  if args.health:
660
- from health_check import get_system_health, print_health_report
661
609
  health = get_system_health()
662
610
  print_health_report(health)
663
611
  return 0 if health.all_healthy else 1
@@ -12,7 +12,7 @@ import sys
12
12
  from pathlib import Path
13
13
  from typing import List
14
14
 
15
- from validator_base import Violation
15
+ from .validator_base import Violation
16
16
 
17
17
 
18
18
  SECRET_PATTERNS: frozenset[str] = frozenset({
@@ -5,11 +5,11 @@ from pathlib import Path
5
5
 
6
6
  import pytest
7
7
 
8
- from abbreviation_checks import (
8
+ from .abbreviation_checks import (
9
9
  check_single_letter_variables,
10
10
  validate_file,
11
11
  )
12
- from validator_base import Violation
12
+ from .validator_base import Violation
13
13
 
14
14
 
15
15
  GOOD_DESCRIPTIVE_NAMES = '''
@@ -6,12 +6,12 @@ from pathlib import Path
6
6
 
7
7
  import pytest
8
8
 
9
- from code_quality_checks import (
9
+ from .code_quality_checks import (
10
10
  check_function_length,
11
11
  check_nesting_depth,
12
12
  check_file_length,
13
13
  )
14
- from validator_base import Violation
14
+ from .validator_base import Violation
15
15
 
16
16
 
17
17
  GOOD_SHORT_FUNCTION = '''