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.
Files changed (105) 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} +150 -5
  6. package/hooks/blocking/{destructive-command-blocker.py → destructive_command_blocker.py} +12 -4
  7. package/hooks/blocking/{tdd-enforcer.py → tdd_enforcer.py} +12 -0
  8. package/hooks/blocking/test_code_rules_enforcer_any_type_ignore.py +2 -2
  9. package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +2 -2
  10. package/hooks/blocking/test_code_rules_enforcer_conftest_anchor.py +1 -1
  11. package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +2 -2
  12. package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +181 -0
  13. package/hooks/blocking/test_code_rules_enforcer_fstring_scan.py +4 -4
  14. package/hooks/blocking/test_code_rules_enforcer_logger_fstring.py +1 -1
  15. package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
  16. package/hooks/blocking/test_code_rules_enforcer_magic_string_masking.py +104 -0
  17. package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +2 -2
  18. package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +2 -2
  19. package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +1 -1
  20. package/hooks/blocking/test_destructive_command_blocker.py +63 -4
  21. package/hooks/blocking/test_gh_body_arg_blocker.py +1 -1
  22. package/hooks/blocking/test_pr_description_enforcer.py +8 -8
  23. package/hooks/blocking/test_tdd_enforcer.py +53 -1
  24. package/hooks/github-action/pre-push-review.yml +27 -0
  25. package/hooks/hooks.json +28 -28
  26. package/hooks/lifecycle/{config-change-guard.py → config_change_guard.py} +27 -12
  27. package/hooks/lifecycle/test_config_change_guard.py +3 -3
  28. package/hooks/notification/{attention-needed-notify.py → attention_needed_notify.py} +7 -0
  29. package/hooks/notification/{claude-notification-handler.py → claude_notification_handler.py} +8 -0
  30. package/hooks/notification/notification_utils.py +60 -2
  31. package/hooks/notification/subagent_complete_notify.py +381 -0
  32. package/hooks/notification/test_attention_needed_notify.py +47 -0
  33. package/hooks/notification/test_claude_notification_handler.py +54 -0
  34. package/hooks/notification/test_notification_utils.py +91 -0
  35. package/hooks/notification/test_subagent_complete_notify.py +72 -0
  36. package/hooks/validators/README.md +5 -1
  37. package/hooks/validators/abbreviation_checks.py +1 -1
  38. package/hooks/validators/code_quality_checks.py +1 -1
  39. package/hooks/validators/config.py +5 -0
  40. package/hooks/validators/conftest.py +10 -0
  41. package/hooks/validators/exempt_paths.py +1 -1
  42. package/hooks/validators/git_checks.py +80 -0
  43. package/hooks/validators/magic_value_checks.py +2 -2
  44. package/hooks/validators/pr_reference_checks.py +1 -1
  45. package/hooks/validators/python_antipattern_checks.py +1 -1
  46. package/hooks/validators/run_all_validators.py +53 -105
  47. package/hooks/validators/security_checks.py +1 -1
  48. package/hooks/validators/test_abbreviation_checks.py +2 -2
  49. package/hooks/validators/test_code_quality_checks.py +2 -2
  50. package/hooks/validators/test_file_structure_checks.py +1 -1
  51. package/hooks/validators/test_git_checks.py +79 -13
  52. package/hooks/validators/test_health_check.py +1 -1
  53. package/hooks/validators/test_magic_value_checks.py +2 -2
  54. package/hooks/validators/test_mypy_integration.py +1 -1
  55. package/hooks/validators/test_output_formatter.py +3 -1
  56. package/hooks/validators/test_pr_reference_checks.py +2 -2
  57. package/hooks/validators/test_python_antipattern_checks.py +2 -2
  58. package/hooks/validators/test_python_style_checks.py +2 -4
  59. package/hooks/validators/test_react_checks.py +1 -1
  60. package/hooks/validators/test_ruff_integration.py +1 -1
  61. package/hooks/validators/test_run_all_validators.py +75 -43
  62. package/hooks/validators/test_run_all_validators_integration.py +14 -37
  63. package/hooks/validators/test_security_checks.py +2 -2
  64. package/hooks/validators/test_test_safety_checks.py +1 -1
  65. package/hooks/validators/test_todo_checks.py +2 -2
  66. package/hooks/validators/test_type_safety_checks.py +2 -2
  67. package/hooks/validators/test_useless_test_checks.py +2 -2
  68. package/hooks/validators/test_validator_base.py +1 -1
  69. package/hooks/validators/test_verify_paths.py +2 -4
  70. package/hooks/validators/todo_checks.py +1 -1
  71. package/hooks/validators/type_safety_checks.py +1 -1
  72. package/hooks/validators/useless_test_checks.py +1 -1
  73. package/package.json +1 -1
  74. package/rules/file-global-constants.md +71 -0
  75. package/rules/gh-body-file.md +1 -1
  76. package/rules/prompt-workflow-context-controls.md +48 -0
  77. package/scripts/sync_to_cursor/rules.py +2 -2
  78. package/scripts/tests/test_sync_to_cursor.py +2 -2
  79. package/skills/bugteam/CONSTRAINTS.md +37 -0
  80. package/skills/bugteam/EXAMPLES.md +64 -0
  81. package/skills/bugteam/PROMPTS.md +175 -0
  82. package/skills/bugteam/SKILL.md +204 -295
  83. package/skills/bugteam/SKILL_EVALS.md +346 -0
  84. package/skills/bugteam/scripts/README.md +37 -0
  85. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +334 -0
  86. package/skills/bugteam/scripts/bugteam_preflight.py +135 -0
  87. package/skills/rule-audit/SKILL.md +4 -4
  88. /package/hooks/advisory/{migration-safety-advisor.py → migration_safety_advisor.py} +0 -0
  89. /package/hooks/advisory/{refactor-guard.py → refactor_guard.py} +0 -0
  90. /package/hooks/blocking/{block-main-commit.py → block_main_commit.py} +0 -0
  91. /package/hooks/blocking/{content-search-to-zoekt-redirector.py → content_search_to_zoekt_redirector.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/{test-preflight-check.py → test_preflight_check.py} +0 -0
  97. /package/hooks/blocking/{write-existing-file-blocker.py → write_existing_file_blocker.py} +0 -0
  98. /package/hooks/git-hooks/{post-commit.py → post_commit.py} +0 -0
  99. /package/hooks/lifecycle/{session-end-cleanup.py → session_end_cleanup.py} +0 -0
  100. /package/hooks/{rewrite-plugin-paths.py → rewrite_plugin_paths.py} +0 -0
  101. /package/hooks/session/{plugin-data-dir-cleanup.py → plugin_data_dir_cleanup.py} +0 -0
  102. /package/hooks/validation/{hook-format-validator.py → hook_format_validator.py} +0 -0
  103. /package/hooks/workflow/{auto-formatter.py → auto_formatter.py} +0 -0
  104. /package/hooks/workflow/{investigation-tracker-reset.py → investigation_tracker_reset.py} +0 -0
  105. /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:
@@ -2,7 +2,7 @@
2
2
 
3
3
  import pytest
4
4
  from pathlib import Path
5
- from react_checks import check_no_class_components, Violation
5
+ from .react_checks import check_no_class_components, Violation
6
6
 
7
7
 
8
8
  def test_class_component_extends_component_should_fail(tmp_path: Path) -> None:
@@ -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
- class TestNewValidatorsIntegration:
14
- def test_abbreviation_checks_called(self) -> None:
15
- """Verify abbreviation_checks is invoked by run_all_validators."""
16
- result = subprocess.run(
17
- [sys.executable, str(VALIDATORS_DIR / "run_all_validators.py"), "--help"],
18
- capture_output=True,
19
- text=True,
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
- def test_magic_value_checks_called(self) -> None:
33
- """Verify magic_value_checks is invoked by run_all_validators."""
34
- result = subprocess.run(
35
- [sys.executable, str(VALIDATORS_DIR / "run_all_validators.py"), "--help"],
36
- capture_output=True,
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 = '''
@@ -7,7 +7,7 @@ from typing import List
7
7
 
8
8
  import pytest
9
9
 
10
- from test_safety_checks import (
10
+ from .test_safety_checks import (
11
11
  Violation,
12
12
  check_no_skip_decorators,
13
13
  check_debug_guard_in_dev_scripts,
@@ -2,8 +2,8 @@
2
2
 
3
3
  import pytest
4
4
 
5
- from todo_checks import check_untracked_todos
6
- from validator_base import Violation
5
+ from .todo_checks import check_untracked_todos
6
+ from .validator_base import Violation
7
7
 
8
8
 
9
9
  GOOD_TODO_WITH_ISSUE = '''
@@ -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 = '''
@@ -4,10 +4,10 @@ import ast
4
4
 
5
5
  import pytest
6
6
 
7
- from useless_test_checks import (
7
+ from .useless_test_checks import (
8
8
  check_useless_tests,
9
9
  )
10
- from validator_base import Violation
10
+ from .validator_base import Violation
11
11
 
12
12
 
13
13
  GOOD_BEHAVIOR_TEST = '''
@@ -2,7 +2,7 @@
2
2
 
3
3
  import pytest
4
4
 
5
- from validator_base import Violation
5
+ from .validator_base import Violation
6
6
 
7
7
 
8
8
  class TestViolation:
@@ -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`
@@ -8,7 +8,7 @@ import sys
8
8
  from pathlib import Path
9
9
  from typing import List
10
10
 
11
- from validator_base import Violation
11
+ from .validator_base import Violation
12
12
 
13
13
 
14
14
  TODO_PATTERN = re.compile(r"#\s*(TODO|FIXME)\b(?!.*#\d+)", re.IGNORECASE)
@@ -10,7 +10,7 @@ import sys
10
10
  from pathlib import Path
11
11
  from typing import List
12
12
 
13
- from validator_base import Violation
13
+ from .validator_base import Violation
14
14
 
15
15
 
16
16
  def check_missing_type_hints(tree: ast.AST, filename: str) -> List[Violation]:
@@ -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
  def check_useless_tests(tree: ast.AST, filename: str) -> List[Violation]:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.25.1",
3
+ "version": "1.26.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
+ ```
@@ -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 (`gh-body-arg-blocker.py`) blocks any Bash call that uses
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 sync-to-cursor.py — do not edit directly -->\n"
211
- "<!-- Re-run: python ~/.claude/scripts/sync-to-cursor.py -->\n"
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 sync-to-cursor.py: canonical docs copy, manifest, and truncation footer."""
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 / "sync-to-cursor.py"
18
+ _SYNC_SCRIPT = _SCRIPTS_DIR / "sync_to_cursor.py"
19
19
 
20
20
 
21
21
  def _minimal_rule_files(claude_rules: Path) -> None: