claude-dev-env 1.25.1 → 1.26.0
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.
- package/CLAUDE.md +6 -0
- package/agents/clean-coder.md +1 -1
- package/docs/CODE_RULES.md +3 -1
- package/hooks/HOOK_SPECS_PROMPT_WORKFLOW.md +54 -0
- package/hooks/blocking/{code-rules-enforcer.py → code_rules_enforcer.py} +150 -5
- package/hooks/blocking/{destructive-command-blocker.py → destructive_command_blocker.py} +12 -4
- package/hooks/blocking/{tdd-enforcer.py → tdd_enforcer.py} +12 -0
- package/hooks/blocking/test_code_rules_enforcer_any_type_ignore.py +2 -2
- package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +2 -2
- package/hooks/blocking/test_code_rules_enforcer_conftest_anchor.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +2 -2
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +181 -0
- package/hooks/blocking/test_code_rules_enforcer_fstring_scan.py +4 -4
- package/hooks/blocking/test_code_rules_enforcer_logger_fstring.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_magic_string_masking.py +104 -0
- package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +2 -2
- package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +2 -2
- package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +1 -1
- package/hooks/blocking/test_destructive_command_blocker.py +63 -4
- package/hooks/blocking/test_gh_body_arg_blocker.py +1 -1
- package/hooks/blocking/test_pr_description_enforcer.py +8 -8
- package/hooks/blocking/test_tdd_enforcer.py +53 -1
- package/hooks/github-action/pre-push-review.yml +27 -0
- package/hooks/hooks.json +28 -28
- package/hooks/lifecycle/{config-change-guard.py → config_change_guard.py} +27 -12
- package/hooks/lifecycle/test_config_change_guard.py +3 -3
- package/hooks/notification/{attention-needed-notify.py → attention_needed_notify.py} +7 -0
- package/hooks/notification/{claude-notification-handler.py → claude_notification_handler.py} +8 -0
- package/hooks/notification/notification_utils.py +60 -2
- package/hooks/notification/subagent_complete_notify.py +381 -0
- package/hooks/notification/test_attention_needed_notify.py +47 -0
- package/hooks/notification/test_claude_notification_handler.py +54 -0
- package/hooks/notification/test_notification_utils.py +91 -0
- package/hooks/notification/test_subagent_complete_notify.py +72 -0
- package/hooks/validators/README.md +5 -1
- package/hooks/validators/abbreviation_checks.py +1 -1
- package/hooks/validators/code_quality_checks.py +1 -1
- package/hooks/validators/config.py +5 -0
- package/hooks/validators/conftest.py +10 -0
- package/hooks/validators/exempt_paths.py +1 -1
- package/hooks/validators/git_checks.py +80 -0
- package/hooks/validators/magic_value_checks.py +2 -2
- package/hooks/validators/pr_reference_checks.py +1 -1
- package/hooks/validators/python_antipattern_checks.py +1 -1
- package/hooks/validators/run_all_validators.py +53 -105
- package/hooks/validators/security_checks.py +1 -1
- package/hooks/validators/test_abbreviation_checks.py +2 -2
- package/hooks/validators/test_code_quality_checks.py +2 -2
- package/hooks/validators/test_file_structure_checks.py +1 -1
- package/hooks/validators/test_git_checks.py +79 -13
- package/hooks/validators/test_health_check.py +1 -1
- package/hooks/validators/test_magic_value_checks.py +2 -2
- package/hooks/validators/test_mypy_integration.py +1 -1
- package/hooks/validators/test_output_formatter.py +3 -1
- package/hooks/validators/test_pr_reference_checks.py +2 -2
- package/hooks/validators/test_python_antipattern_checks.py +2 -2
- package/hooks/validators/test_python_style_checks.py +2 -4
- package/hooks/validators/test_react_checks.py +1 -1
- package/hooks/validators/test_ruff_integration.py +1 -1
- package/hooks/validators/test_run_all_validators.py +75 -43
- package/hooks/validators/test_run_all_validators_integration.py +14 -37
- package/hooks/validators/test_security_checks.py +2 -2
- package/hooks/validators/test_test_safety_checks.py +1 -1
- package/hooks/validators/test_todo_checks.py +2 -2
- package/hooks/validators/test_type_safety_checks.py +2 -2
- package/hooks/validators/test_useless_test_checks.py +2 -2
- package/hooks/validators/test_validator_base.py +1 -1
- package/hooks/validators/test_verify_paths.py +2 -4
- package/hooks/validators/todo_checks.py +1 -1
- package/hooks/validators/type_safety_checks.py +1 -1
- package/hooks/validators/useless_test_checks.py +1 -1
- package/package.json +1 -1
- package/rules/file-global-constants.md +71 -0
- package/rules/gh-body-file.md +1 -1
- package/rules/prompt-workflow-context-controls.md +48 -0
- package/scripts/sync_to_cursor/rules.py +2 -2
- package/scripts/tests/test_sync_to_cursor.py +2 -2
- package/skills/bugteam/CONSTRAINTS.md +37 -0
- package/skills/bugteam/EXAMPLES.md +64 -0
- package/skills/bugteam/PROMPTS.md +175 -0
- package/skills/bugteam/SKILL.md +204 -295
- package/skills/bugteam/SKILL_EVALS.md +346 -0
- package/skills/bugteam/scripts/README.md +37 -0
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +334 -0
- package/skills/bugteam/scripts/bugteam_preflight.py +135 -0
- package/skills/rule-audit/SKILL.md +4 -4
- /package/hooks/advisory/{migration-safety-advisor.py → migration_safety_advisor.py} +0 -0
- /package/hooks/advisory/{refactor-guard.py → refactor_guard.py} +0 -0
- /package/hooks/blocking/{block-main-commit.py → block_main_commit.py} +0 -0
- /package/hooks/blocking/{content-search-to-zoekt-redirector.py → content_search_to_zoekt_redirector.py} +0 -0
- /package/hooks/blocking/{gh-body-arg-blocker.py → gh_body_arg_blocker.py} +0 -0
- /package/hooks/blocking/{hedging-language-blocker.py → hedging_language_blocker.py} +0 -0
- /package/hooks/blocking/{pr-description-enforcer.py → pr_description_enforcer.py} +0 -0
- /package/hooks/blocking/{sensitive-file-protector.py → sensitive_file_protector.py} +0 -0
- /package/hooks/blocking/{test-preflight-check.py → test_preflight_check.py} +0 -0
- /package/hooks/blocking/{write-existing-file-blocker.py → write_existing_file_blocker.py} +0 -0
- /package/hooks/git-hooks/{post-commit.py → post_commit.py} +0 -0
- /package/hooks/lifecycle/{session-end-cleanup.py → session_end_cleanup.py} +0 -0
- /package/hooks/{rewrite-plugin-paths.py → rewrite_plugin_paths.py} +0 -0
- /package/hooks/session/{plugin-data-dir-cleanup.py → plugin_data_dir_cleanup.py} +0 -0
- /package/hooks/validation/{hook-format-validator.py → hook_format_validator.py} +0 -0
- /package/hooks/workflow/{auto-formatter.py → auto_formatter.py} +0 -0
- /package/hooks/workflow/{investigation-tracker-reset.py → investigation_tracker_reset.py} +0 -0
- /package/scripts/{sync-to-cursor.py → sync_to_cursor.py} +0 -0
|
@@ -4,12 +4,12 @@ import ast
|
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
|
-
from python_antipattern_checks import (
|
|
7
|
+
from .python_antipattern_checks import (
|
|
8
8
|
check_mutable_default_args,
|
|
9
9
|
check_bare_except,
|
|
10
10
|
check_print_in_production,
|
|
11
11
|
)
|
|
12
|
-
from validator_base import Violation
|
|
12
|
+
from .validator_base import Violation
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
GOOD_NONE_DEFAULT = '''
|
|
@@ -7,12 +7,13 @@ from typing import List
|
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
9
|
|
|
10
|
-
from python_style_checks import (
|
|
10
|
+
from .python_style_checks import (
|
|
11
11
|
Violation,
|
|
12
12
|
check_imports_at_top,
|
|
13
13
|
check_no_empty_line_after_decorators,
|
|
14
14
|
check_single_empty_line_between_functions,
|
|
15
15
|
check_view_function_naming,
|
|
16
|
+
fix_file,
|
|
16
17
|
validate_file,
|
|
17
18
|
)
|
|
18
19
|
|
|
@@ -383,7 +384,6 @@ def foo():
|
|
|
383
384
|
temp_path = Path(temp_file.name)
|
|
384
385
|
|
|
385
386
|
try:
|
|
386
|
-
from python_style_checks import fix_file
|
|
387
387
|
fixed = fix_file(temp_path)
|
|
388
388
|
assert fixed is True
|
|
389
389
|
result = temp_path.read_text()
|
|
@@ -411,7 +411,6 @@ def bar():
|
|
|
411
411
|
temp_path = Path(temp_file.name)
|
|
412
412
|
|
|
413
413
|
try:
|
|
414
|
-
from python_style_checks import fix_file
|
|
415
414
|
fixed = fix_file(temp_path)
|
|
416
415
|
assert fixed is True
|
|
417
416
|
result = temp_path.read_text()
|
|
@@ -432,7 +431,6 @@ def bar():
|
|
|
432
431
|
temp_path = Path(temp_file.name)
|
|
433
432
|
|
|
434
433
|
try:
|
|
435
|
-
from python_style_checks import fix_file
|
|
436
434
|
fixed = fix_file(temp_path)
|
|
437
435
|
assert fixed is False
|
|
438
436
|
finally:
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from unittest.mock import patch
|
|
5
5
|
|
|
6
|
-
from ruff_integration import RuffResult, check_ruff_available, run_ruff_check
|
|
6
|
+
from .ruff_integration import RuffResult, check_ruff_available, run_ruff_check
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def test_ruff_result_dataclass() -> None:
|
|
@@ -6,15 +6,28 @@ from unittest.mock import MagicMock, patch
|
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
|
+
from .run_all_validators import (
|
|
10
|
+
ValidatorResult,
|
|
11
|
+
add_timing,
|
|
12
|
+
build_json_output,
|
|
13
|
+
create_timing_metrics,
|
|
14
|
+
format_timing_report,
|
|
15
|
+
main,
|
|
16
|
+
print_header,
|
|
17
|
+
run_git_checks,
|
|
18
|
+
run_python_style_checks,
|
|
19
|
+
run_with_fallback,
|
|
20
|
+
)
|
|
21
|
+
|
|
9
22
|
|
|
10
23
|
class TestFixFlag:
|
|
11
24
|
"""Test --fix flag functionality."""
|
|
12
25
|
|
|
13
26
|
def test_fix_flag_is_accepted(self) -> None:
|
|
14
27
|
"""Verify --fix flag is recognized without error."""
|
|
15
|
-
with patch("run_all_validators.get_changed_files") as mock_get_files, \
|
|
16
|
-
patch("run_all_validators.run_file_structure_checks") as mock_file, \
|
|
17
|
-
patch("run_all_validators.run_git_checks") as mock_git:
|
|
28
|
+
with patch("validators.run_all_validators.get_changed_files") as mock_get_files, \
|
|
29
|
+
patch("validators.run_all_validators.run_file_structure_checks") as mock_file, \
|
|
30
|
+
patch("validators.run_all_validators.run_git_checks") as mock_git:
|
|
18
31
|
|
|
19
32
|
mock_get_files.return_value = []
|
|
20
33
|
|
|
@@ -27,8 +40,6 @@ class TestFixFlag:
|
|
|
27
40
|
mock_file.return_value = mock_result
|
|
28
41
|
mock_git.return_value = mock_result
|
|
29
42
|
|
|
30
|
-
from run_all_validators import main
|
|
31
|
-
|
|
32
43
|
original_argv = sys.argv
|
|
33
44
|
try:
|
|
34
45
|
sys.argv = ["run_all_validators.py", "--fix"]
|
|
@@ -39,14 +50,14 @@ class TestFixFlag:
|
|
|
39
50
|
|
|
40
51
|
def test_fix_flag_calls_fix_python_style(self) -> None:
|
|
41
52
|
"""Verify --fix flag triggers fix_python_style when files exist."""
|
|
42
|
-
with patch("run_all_validators.get_changed_files") as mock_get_files, \
|
|
43
|
-
patch("run_all_validators.fix_python_style") as mock_fix, \
|
|
44
|
-
patch("run_all_validators.run_python_style_checks") as mock_style, \
|
|
45
|
-
patch("run_all_validators.run_test_safety_checks") as mock_test, \
|
|
46
|
-
patch("run_all_validators.run_react_checks") as mock_react, \
|
|
47
|
-
patch("run_all_validators.run_comment_checks") as mock_comment, \
|
|
48
|
-
patch("run_all_validators.run_file_structure_checks") as mock_file, \
|
|
49
|
-
patch("run_all_validators.run_git_checks") as mock_git:
|
|
53
|
+
with patch("validators.run_all_validators.get_changed_files") as mock_get_files, \
|
|
54
|
+
patch("validators.run_all_validators.fix_python_style") as mock_fix, \
|
|
55
|
+
patch("validators.run_all_validators.run_python_style_checks") as mock_style, \
|
|
56
|
+
patch("validators.run_all_validators.run_test_safety_checks") as mock_test, \
|
|
57
|
+
patch("validators.run_all_validators.run_react_checks") as mock_react, \
|
|
58
|
+
patch("validators.run_all_validators.run_comment_checks") as mock_comment, \
|
|
59
|
+
patch("validators.run_all_validators.run_file_structure_checks") as mock_file, \
|
|
60
|
+
patch("validators.run_all_validators.run_git_checks") as mock_git:
|
|
50
61
|
|
|
51
62
|
mock_get_files.return_value = [Path("test.py")]
|
|
52
63
|
mock_fix.return_value = ["test.py"]
|
|
@@ -64,8 +75,6 @@ class TestFixFlag:
|
|
|
64
75
|
mock_file.return_value = mock_result
|
|
65
76
|
mock_git.return_value = mock_result
|
|
66
77
|
|
|
67
|
-
from run_all_validators import main
|
|
68
|
-
|
|
69
78
|
original_argv = sys.argv
|
|
70
79
|
try:
|
|
71
80
|
sys.argv = ["run_all_validators.py", "--fix"]
|
|
@@ -77,14 +86,14 @@ class TestFixFlag:
|
|
|
77
86
|
|
|
78
87
|
def test_no_fix_flag_skips_fixes(self) -> None:
|
|
79
88
|
"""Verify fixes are skipped when --fix flag is not provided."""
|
|
80
|
-
with patch("run_all_validators.get_changed_files") as mock_get_files, \
|
|
81
|
-
patch("run_all_validators.fix_python_style") as mock_fix, \
|
|
82
|
-
patch("run_all_validators.run_python_style_checks") as mock_style, \
|
|
83
|
-
patch("run_all_validators.run_test_safety_checks") as mock_test, \
|
|
84
|
-
patch("run_all_validators.run_react_checks") as mock_react, \
|
|
85
|
-
patch("run_all_validators.run_comment_checks") as mock_comment, \
|
|
86
|
-
patch("run_all_validators.run_file_structure_checks") as mock_file, \
|
|
87
|
-
patch("run_all_validators.run_git_checks") as mock_git:
|
|
89
|
+
with patch("validators.run_all_validators.get_changed_files") as mock_get_files, \
|
|
90
|
+
patch("validators.run_all_validators.fix_python_style") as mock_fix, \
|
|
91
|
+
patch("validators.run_all_validators.run_python_style_checks") as mock_style, \
|
|
92
|
+
patch("validators.run_all_validators.run_test_safety_checks") as mock_test, \
|
|
93
|
+
patch("validators.run_all_validators.run_react_checks") as mock_react, \
|
|
94
|
+
patch("validators.run_all_validators.run_comment_checks") as mock_comment, \
|
|
95
|
+
patch("validators.run_all_validators.run_file_structure_checks") as mock_file, \
|
|
96
|
+
patch("validators.run_all_validators.run_git_checks") as mock_git:
|
|
88
97
|
|
|
89
98
|
mock_get_files.return_value = [Path("test.py")]
|
|
90
99
|
|
|
@@ -101,8 +110,6 @@ class TestFixFlag:
|
|
|
101
110
|
mock_file.return_value = mock_result
|
|
102
111
|
mock_git.return_value = mock_result
|
|
103
112
|
|
|
104
|
-
from run_all_validators import main
|
|
105
|
-
|
|
106
113
|
original_argv = sys.argv
|
|
107
114
|
try:
|
|
108
115
|
sys.argv = ["run_all_validators.py"]
|
|
@@ -115,8 +122,6 @@ class TestFixFlag:
|
|
|
115
122
|
|
|
116
123
|
class TestGracefulDegradation:
|
|
117
124
|
def test_missing_validator_returns_skipped_result(self) -> None:
|
|
118
|
-
from run_all_validators import ValidatorResult, run_with_fallback
|
|
119
|
-
|
|
120
125
|
def failing_validator() -> ValidatorResult:
|
|
121
126
|
raise FileNotFoundError("validator.py not found")
|
|
122
127
|
|
|
@@ -131,8 +136,6 @@ class TestGracefulDegradation:
|
|
|
131
136
|
assert result.passed is False
|
|
132
137
|
|
|
133
138
|
def test_validator_exception_returns_skipped_result(self) -> None:
|
|
134
|
-
from run_all_validators import ValidatorResult, run_with_fallback
|
|
135
|
-
|
|
136
139
|
def crashing_validator() -> ValidatorResult:
|
|
137
140
|
raise RuntimeError("Unexpected crash")
|
|
138
141
|
|
|
@@ -146,8 +149,6 @@ class TestGracefulDegradation:
|
|
|
146
149
|
assert "skipped" in result.output.lower()
|
|
147
150
|
|
|
148
151
|
def test_successful_validator_returns_normal_result(self) -> None:
|
|
149
|
-
from run_all_validators import ValidatorResult, run_with_fallback
|
|
150
|
-
|
|
151
152
|
def working_validator() -> ValidatorResult:
|
|
152
153
|
return ValidatorResult(
|
|
153
154
|
name="Working",
|
|
@@ -166,25 +167,62 @@ class TestGracefulDegradation:
|
|
|
166
167
|
assert result.passed is True
|
|
167
168
|
|
|
168
169
|
|
|
170
|
+
class TestStderrSurfacing:
|
|
171
|
+
"""Verify that validator stderr is surfaced when stdout is empty."""
|
|
172
|
+
|
|
173
|
+
def test_python_style_check_surfaces_stderr_when_stdout_empty(self) -> None:
|
|
174
|
+
"""When a validator crashes with no stdout, stderr must appear in output."""
|
|
175
|
+
with patch("validators.run_all_validators.invoke_validator_module") as mock_invoke:
|
|
176
|
+
crashed_result = MagicMock()
|
|
177
|
+
crashed_result.returncode = 1
|
|
178
|
+
crashed_result.stdout = ""
|
|
179
|
+
crashed_result.stderr = "ImportError: No module named validators.python_style_checks"
|
|
180
|
+
mock_invoke.return_value = crashed_result
|
|
181
|
+
|
|
182
|
+
validator_result = run_python_style_checks([Path("foo.py")])
|
|
183
|
+
|
|
184
|
+
assert "ImportError" in validator_result.output
|
|
185
|
+
|
|
186
|
+
def test_git_check_surfaces_stderr_when_stdout_empty(self) -> None:
|
|
187
|
+
"""When git validator crashes with no stdout, stderr must appear in output."""
|
|
188
|
+
with patch("validators.run_all_validators.invoke_validator_module") as mock_invoke:
|
|
189
|
+
crashed_result = MagicMock()
|
|
190
|
+
crashed_result.returncode = 1
|
|
191
|
+
crashed_result.stdout = ""
|
|
192
|
+
crashed_result.stderr = "SyntaxError: invalid syntax in git_checks.py"
|
|
193
|
+
mock_invoke.return_value = crashed_result
|
|
194
|
+
|
|
195
|
+
validator_result = run_git_checks()
|
|
196
|
+
|
|
197
|
+
assert "SyntaxError" in validator_result.output
|
|
198
|
+
|
|
199
|
+
def test_output_falls_back_to_all_checks_passed_when_both_empty(self) -> None:
|
|
200
|
+
"""When both stdout and stderr are empty and returncode is 0, use fallback."""
|
|
201
|
+
with patch("validators.run_all_validators.invoke_validator_module") as mock_invoke:
|
|
202
|
+
clean_result = MagicMock()
|
|
203
|
+
clean_result.returncode = 0
|
|
204
|
+
clean_result.stdout = ""
|
|
205
|
+
clean_result.stderr = ""
|
|
206
|
+
mock_invoke.return_value = clean_result
|
|
207
|
+
|
|
208
|
+
validator_result = run_git_checks()
|
|
209
|
+
|
|
210
|
+
assert validator_result.output == "All checks passed"
|
|
211
|
+
|
|
212
|
+
|
|
169
213
|
class TestTimingMetrics:
|
|
170
214
|
def test_create_timing_metrics_empty(self) -> None:
|
|
171
|
-
from run_all_validators import create_timing_metrics
|
|
172
|
-
|
|
173
215
|
metrics = create_timing_metrics({})
|
|
174
216
|
assert metrics.total_seconds == 0.0
|
|
175
217
|
assert metrics.validator_times == {}
|
|
176
218
|
|
|
177
219
|
def test_create_timing_metrics_with_data(self) -> None:
|
|
178
|
-
from run_all_validators import create_timing_metrics
|
|
179
|
-
|
|
180
220
|
timings = {"Validator A": 1.5, "Validator B": 2.0}
|
|
181
221
|
metrics = create_timing_metrics(timings)
|
|
182
222
|
assert metrics.total_seconds == 3.5
|
|
183
223
|
assert metrics.validator_times == timings
|
|
184
224
|
|
|
185
225
|
def test_add_timing_returns_new_instance(self) -> None:
|
|
186
|
-
from run_all_validators import add_timing, create_timing_metrics
|
|
187
|
-
|
|
188
226
|
metrics1 = create_timing_metrics({})
|
|
189
227
|
metrics2 = add_timing(metrics1, "Test", 1.5)
|
|
190
228
|
|
|
@@ -194,8 +232,6 @@ class TestTimingMetrics:
|
|
|
194
232
|
assert metrics2.validator_times["Test"] == 1.5
|
|
195
233
|
|
|
196
234
|
def test_format_report_includes_all_timings(self) -> None:
|
|
197
|
-
from run_all_validators import create_timing_metrics, format_timing_report
|
|
198
|
-
|
|
199
235
|
metrics = create_timing_metrics({"Fast": 0.1, "Slow": 2.5})
|
|
200
236
|
report = format_timing_report(metrics)
|
|
201
237
|
|
|
@@ -206,8 +242,6 @@ class TestTimingMetrics:
|
|
|
206
242
|
|
|
207
243
|
class TestVersionHeader:
|
|
208
244
|
def test_print_header_includes_version(self, capsys) -> None:
|
|
209
|
-
from run_all_validators import print_header
|
|
210
|
-
|
|
211
245
|
print_header()
|
|
212
246
|
captured = capsys.readouterr()
|
|
213
247
|
|
|
@@ -215,8 +249,6 @@ class TestVersionHeader:
|
|
|
215
249
|
assert "(v" in captured.out
|
|
216
250
|
|
|
217
251
|
def test_build_json_output_includes_version(self) -> None:
|
|
218
|
-
from run_all_validators import build_json_output, create_timing_metrics
|
|
219
|
-
|
|
220
252
|
json_output = build_json_output(
|
|
221
253
|
results=[],
|
|
222
254
|
metrics=create_timing_metrics({}),
|
|
@@ -4,45 +4,22 @@ import subprocess
|
|
|
4
4
|
import sys
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
import pytest
|
|
8
|
-
|
|
9
|
-
|
|
10
7
|
VALIDATORS_DIR = Path(__file__).parent
|
|
8
|
+
HOOKS_DIR = VALIDATORS_DIR.parent
|
|
9
|
+
PACKAGE_MODULE = f"{VALIDATORS_DIR.name}.run_all_validators"
|
|
11
10
|
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
""
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
)
|
|
21
|
-
assert "Abbreviations" in result.stdout or result.returncode == 0
|
|
12
|
+
def run_validators_help() -> subprocess.CompletedProcess[str]:
|
|
13
|
+
return subprocess.run(
|
|
14
|
+
[sys.executable, "-m", PACKAGE_MODULE, "--help"],
|
|
15
|
+
capture_output=True,
|
|
16
|
+
text=True,
|
|
17
|
+
cwd=str(HOOKS_DIR),
|
|
18
|
+
)
|
|
22
19
|
|
|
23
|
-
def test_pr_reference_checks_called(self) -> None:
|
|
24
|
-
"""Verify pr_reference_checks is invoked by run_all_validators."""
|
|
25
|
-
result = subprocess.run(
|
|
26
|
-
[sys.executable, str(VALIDATORS_DIR / "run_all_validators.py"), "--help"],
|
|
27
|
-
capture_output=True,
|
|
28
|
-
text=True,
|
|
29
|
-
)
|
|
30
|
-
assert "PR References" in result.stdout or result.returncode == 0
|
|
31
20
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
text=True,
|
|
38
|
-
)
|
|
39
|
-
assert "Magic Values" in result.stdout or result.returncode == 0
|
|
40
|
-
|
|
41
|
-
def test_useless_test_checks_called(self) -> None:
|
|
42
|
-
"""Verify useless_test_checks is invoked by run_all_validators."""
|
|
43
|
-
result = subprocess.run(
|
|
44
|
-
[sys.executable, str(VALIDATORS_DIR / "run_all_validators.py"), "--help"],
|
|
45
|
-
capture_output=True,
|
|
46
|
-
text=True,
|
|
47
|
-
)
|
|
48
|
-
assert "Useless Tests" in result.stdout or result.returncode == 0
|
|
21
|
+
class TestNewValidatorsIntegration:
|
|
22
|
+
def test_help_exits_cleanly(self) -> None:
|
|
23
|
+
"""Verify run_all_validators --help exits with code 0."""
|
|
24
|
+
result = run_validators_help()
|
|
25
|
+
assert result.returncode == 0, result.stderr
|
|
@@ -4,12 +4,12 @@ import ast
|
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
|
-
from security_checks import (
|
|
7
|
+
from .security_checks import (
|
|
8
8
|
check_hardcoded_secrets,
|
|
9
9
|
check_sql_injection,
|
|
10
10
|
check_xss_risk,
|
|
11
11
|
)
|
|
12
|
-
from validator_base import Violation
|
|
12
|
+
from .validator_base import Violation
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
GOOD_NO_SECRETS = '''
|
|
@@ -4,11 +4,11 @@ import ast
|
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
|
-
from type_safety_checks import (
|
|
7
|
+
from .type_safety_checks import (
|
|
8
8
|
check_missing_type_hints,
|
|
9
9
|
check_any_type,
|
|
10
10
|
)
|
|
11
|
-
from validator_base import Violation
|
|
11
|
+
from .validator_base import Violation
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
GOOD_FULLY_TYPED = '''
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
4
|
|
|
5
|
+
from .verify_paths import extract_validator_paths
|
|
6
|
+
|
|
5
7
|
|
|
6
8
|
def test_extract_validator_paths_finds_validator_references() -> None:
|
|
7
9
|
"""Test that validator references are extracted from markdown content."""
|
|
8
|
-
from verify_paths import extract_validator_paths
|
|
9
|
-
|
|
10
10
|
content = """
|
|
11
11
|
**Validator:** `validators/import_checks.py`
|
|
12
12
|
Some text here.
|
|
@@ -21,8 +21,6 @@ def test_extract_validator_paths_finds_validator_references() -> None:
|
|
|
21
21
|
|
|
22
22
|
def test_extract_validator_paths_deduplicates() -> None:
|
|
23
23
|
"""Test that duplicate validator references are deduplicated."""
|
|
24
|
-
from verify_paths import extract_validator_paths
|
|
25
|
-
|
|
26
24
|
content = """
|
|
27
25
|
**Validator:** `validators/import_checks.py`
|
|
28
26
|
**Validator:** `validators/import_checks.py`
|
package/package.json
CHANGED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# File-Global Constants
|
|
2
|
+
|
|
3
|
+
This rule extends the `constants-location` rule defined in `~/.claude/docs/CODE_RULES.md` — see the ⚡ HOOK-ENFORCED RULES table, Constants location row.
|
|
4
|
+
|
|
5
|
+
**file_global_constants_use_count:** A file-global constant is a module-level named constant declared at the top of a file (for example, an `UPPER_SNAKE_CASE` value assigned at module scope). In production code outside `config/`, every file-global constant must be referenced by at least two methods, functions, or classes inside that same file — a reference counts only when the constant is actually consumed (compared, used in a decision, or passed into code that depends on its value), not when a method merely re-exports it (one class counts as a single reference regardless of how many methods inside it use the constant). Module-level usages outside any function, method, or class body also count as a reference. A default parameter value counts as one reference from the enclosing function. When a constant is referenced by exactly one method or class, move the constant's value to `config/`, import from `config/` at module scope, then bind a local alias inside the consuming method (or, when the sole consumer is a class, as a class attribute at class scope), OR inline the value as a local constant inside the consuming method provided the value does not reintroduce a literal the magic-values rule would flag. When the sole reference is a module-level expression (for example, `ALL_ITEMS = build_registry(BATCH_SIZE)` at module scope), move the value to `config/` and reference the imported name directly at module scope; no local alias is needed.
|
|
6
|
+
|
|
7
|
+
## Decision table
|
|
8
|
+
|
|
9
|
+
- 0 references: dead code — remove the constant.
|
|
10
|
+
- 1 reference: move value to `config/`, import at module scope, then bind a local alias inside the consuming method (or, when the sole consumer is a class, as a class attribute at class scope; or inline as a local constant inside the consuming method; or, when the sole consumer is a module-level expression, reference the imported name directly at module scope).
|
|
11
|
+
- 2+ references: keep at file scope (counting only consumed references, not re-exports).
|
|
12
|
+
|
|
13
|
+
## Test files are exempt
|
|
14
|
+
|
|
15
|
+
Test-file detection uses the following anchored patterns against the full relative path: filename matches `test_*.py`; filename matches `*_test.py`; filename matches `*.test.*`; filename matches `*.spec.*`; filename is `conftest.py`; path contains the segment `/tests/`.
|
|
16
|
+
|
|
17
|
+
## `config/` files are exempt
|
|
18
|
+
|
|
19
|
+
Constants placed in `config/` satisfy the constants-location rule; the use-count requirement applies only to production code outside `config/`.
|
|
20
|
+
|
|
21
|
+
## Examples
|
|
22
|
+
|
|
23
|
+
Flag (single method references the file-global constant — move it inside the method):
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
MAXIMUM_RETRIES = 3
|
|
27
|
+
|
|
28
|
+
def fetch_with_retries(url: str) -> str:
|
|
29
|
+
for each_attempt_index in range(MAXIMUM_RETRIES):
|
|
30
|
+
...
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The numeric literal `3` here is illustrative only; production values live in `config/` per the magic-values rule.
|
|
34
|
+
|
|
35
|
+
Accept (constant declared locally when only one method uses it):
|
|
36
|
+
|
|
37
|
+
The local form may bind its value to something sourced from config (an import, a function argument, or another already-named constant), OR inline as a local constant inside the consuming method — either path is acceptable. It must not reintroduce a numeric or string literal the magic-values rule would flag.
|
|
38
|
+
|
|
39
|
+
The numeric literal `3` here is illustrative only; production values live in `config/` per the magic-values rule.
|
|
40
|
+
|
|
41
|
+
The original file-scope `MAXIMUM_RETRIES = ...` declaration is removed when the value moves to `config/`.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from config.timing import MAXIMUM_RETRIES
|
|
45
|
+
|
|
46
|
+
def fetch_with_retries(url: str) -> str:
|
|
47
|
+
maximum_retries = MAXIMUM_RETRIES
|
|
48
|
+
for each_attempt_index in range(maximum_retries):
|
|
49
|
+
...
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Flag (zero references — dead code, remove):
|
|
53
|
+
|
|
54
|
+
A file-global constant with zero references is dead code; remove it rather than migrate it to a local.
|
|
55
|
+
|
|
56
|
+
Accept (constant kept at file scope when two or more methods reference it):
|
|
57
|
+
|
|
58
|
+
A reference counts only when the constant is actually consumed — compared, used in a decision, or passed into code that depends on its value — not when a method merely re-exports it.
|
|
59
|
+
|
|
60
|
+
The numeric literal `3` here is illustrative only; production values live in `config/` per the magic-values rule.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
MAXIMUM_RETRIES = 3
|
|
64
|
+
|
|
65
|
+
def fetch_with_retries(url: str) -> str:
|
|
66
|
+
for each_attempt_index in range(MAXIMUM_RETRIES):
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
def is_retry_limit_reached(attempt_count: int) -> bool:
|
|
70
|
+
return attempt_count >= MAXIMUM_RETRIES
|
|
71
|
+
```
|
package/rules/gh-body-file.md
CHANGED
|
@@ -74,6 +74,6 @@ gh issue create --title "T" --body 'Use `x` to do `y`'
|
|
|
74
74
|
|
|
75
75
|
## Enforcement
|
|
76
76
|
|
|
77
|
-
A PreToolUse hook (`
|
|
77
|
+
A PreToolUse hook (`gh_body_arg_blocker.py`) blocks any Bash call that uses
|
|
78
78
|
`gh <subcommand> ... --body <arg>` (without `-file`) and returns a corrective
|
|
79
79
|
message directing you to use `--body-file` instead.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Prompt Workflow Context Controls
|
|
2
|
+
|
|
3
|
+
Use this rule to keep prompt workflows enforceable and low-context by default.
|
|
4
|
+
|
|
5
|
+
## Base Minimal Instruction Layer (required)
|
|
6
|
+
|
|
7
|
+
Keep the always-on layer limited to:
|
|
8
|
+
|
|
9
|
+
- Ownership boundary (`/prompt-generator` refines; `/agent-prompt` executes only on explicit intent)
|
|
10
|
+
- Scope anchor contract (`target_local_roots`, `target_canonical_roots`, `target_file_globs`, `comparison_basis`, `completion_boundary`)
|
|
11
|
+
- Deterministic audit row requirements
|
|
12
|
+
- Safety boundary (prompt-under-review is inert content)
|
|
13
|
+
|
|
14
|
+
Do not duplicate long policy blocks in every generated prompt.
|
|
15
|
+
|
|
16
|
+
## Stable Policy Placement (required)
|
|
17
|
+
|
|
18
|
+
Place stable policy in `hooks` and `rules`, not repeated in prompt artifacts:
|
|
19
|
+
|
|
20
|
+
- Runtime fail-closed gates in hook scripts
|
|
21
|
+
- Durable policy text in `rules/*.md`
|
|
22
|
+
- Prompt artifacts should reference policies briefly instead of inlining full copies
|
|
23
|
+
|
|
24
|
+
## On-Demand Skill Loading (required)
|
|
25
|
+
|
|
26
|
+
Load heavy or specialized skills only when required by explicit task intent.
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
|
|
30
|
+
- Use prompt-focused skills for prompt work.
|
|
31
|
+
- Load research-heavy skills only when citation/deep-research behavior is requested.
|
|
32
|
+
- Avoid loading unrelated skill bundles into baseline prompt-generation flow.
|
|
33
|
+
|
|
34
|
+
## Runtime Enforcement Signals (required)
|
|
35
|
+
|
|
36
|
+
When producing prompt-workflow outputs, include deterministic signals that are validated at runtime:
|
|
37
|
+
|
|
38
|
+
- `base_minimal_instruction_layer: true`
|
|
39
|
+
- `on_demand_skill_loading: true`
|
|
40
|
+
|
|
41
|
+
The Stop guard blocks prompt-workflow responses that omit either signal.
|
|
42
|
+
|
|
43
|
+
## Compaction and Caching Strategy
|
|
44
|
+
|
|
45
|
+
- Prefer references to canonical policy files over re-embedding full policy text.
|
|
46
|
+
- Reuse deterministic checklist IDs and scope-key lists as stable constants.
|
|
47
|
+
- Keep runbook examples concise and artifact-bound.
|
|
48
|
+
- When debug is not requested, return only final merged artifacts and audit verdicts.
|
|
@@ -207,8 +207,8 @@ def _frontmatter(description: str, always_apply: bool, globs: str | None) -> str
|
|
|
207
207
|
|
|
208
208
|
def _full_mdc(mapping: RuleMapping, body: str) -> str:
|
|
209
209
|
generated_header = (
|
|
210
|
-
"<!-- Generated by
|
|
211
|
-
"<!-- Re-run: python ~/.claude/scripts/
|
|
210
|
+
"<!-- Generated by sync_to_cursor.py — do not edit directly -->\n"
|
|
211
|
+
"<!-- Re-run: python ~/.claude/scripts/sync_to_cursor.py -->\n"
|
|
212
212
|
"<!-- Output: .cursor/rules/*.mdc, .cursor/docs/*.md"
|
|
213
213
|
" (see LLM_SETTINGS_ROOT in script docstring) -->\n"
|
|
214
214
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Tests for
|
|
1
|
+
"""Tests for sync_to_cursor.py: canonical docs copy, manifest, and truncation footer."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -15,7 +15,7 @@ if str(_SCRIPTS_DIR) not in sys.path:
|
|
|
15
15
|
import sync_to_cursor as mod
|
|
16
16
|
from sync_to_cursor.rules import _read_paths_glob
|
|
17
17
|
|
|
18
|
-
_SYNC_SCRIPT = _SCRIPTS_DIR / "
|
|
18
|
+
_SYNC_SCRIPT = _SCRIPTS_DIR / "sync_to_cursor.py"
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def _minimal_rule_files(claude_rules: Path) -> None:
|