claude-dev-env 1.92.1 → 1.93.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 (214) hide show
  1. package/CLAUDE.md +6 -2
  2. package/_shared/CLAUDE.md +2 -1
  3. package/_shared/advisor/CLAUDE.md +21 -0
  4. package/_shared/advisor/advisor-protocol.md +142 -0
  5. package/_shared/advisor/scripts/config/advisor_scripts_constants/__init__.py +1 -0
  6. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +86 -0
  7. package/_shared/advisor/scripts/model_tier_run_validator.py +246 -0
  8. package/_shared/advisor/scripts/pyproject.toml +3 -0
  9. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +302 -0
  10. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +180 -0
  11. package/_shared/advisor/scripts/tier_model_ids.py +164 -0
  12. package/_shared/pr-loop/CLAUDE.md +1 -0
  13. package/_shared/pr-loop/precatch-rubric.md +65 -0
  14. package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
  15. package/_shared/pr-loop/scripts/code_rules_gate.py +137 -5
  16. package/_shared/pr-loop/scripts/post_audit_thread.py +1 -1
  17. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
  18. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +16 -0
  19. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +2 -1
  20. package/_shared/pr-loop/scripts/reviewer_availability.py +42 -12
  21. package/_shared/pr-loop/scripts/reviews_disabled.py +65 -21
  22. package/_shared/pr-loop/scripts/terminology_sweep.py +69 -21
  23. package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -1
  24. package/_shared/pr-loop/scripts/tests/fixtures/{copilot_internal_user_jonecho.json → copilot_internal_user_example.json} +1 -1
  25. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +1 -1
  26. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +146 -2
  27. package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +8 -8
  28. package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +18 -5
  29. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +56 -5
  30. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +104 -17
  31. package/agents/CLAUDE.md +3 -2
  32. package/agents/clasp-deployment-orchestrator.md +2 -2
  33. package/agents/code-advisor.md +7 -5
  34. package/agents/code-verifier.md +6 -2
  35. package/agents/session-advisor.md +27 -0
  36. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
  37. package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -1
  38. package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
  39. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
  40. package/audit-rubrics/prompts/category-c-resource-cleanup.md +2 -2
  41. package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +2 -2
  42. package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
  43. package/audit-rubrics/prompts/category-f-silent-failures.md +2 -2
  44. package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
  45. package/audit-rubrics/prompts/category-h-security-boundaries.md +2 -2
  46. package/audit-rubrics/prompts/category-i-concurrency.md +2 -2
  47. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +2 -2
  48. package/audit-rubrics/prompts/category-k-codebase-conflicts.md +2 -2
  49. package/audit-rubrics/prompts/category-l-behavior-equivalence.md +2 -2
  50. package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +2 -2
  51. package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +2 -2
  52. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
  53. package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +2 -2
  54. package/bin/CLAUDE.md +1 -0
  55. package/bin/expand_home_directory_tokens.mjs +75 -0
  56. package/bin/install.mjs +21 -7
  57. package/bin/install.test.mjs +220 -10
  58. package/hooks/CLAUDE.md +1 -1
  59. package/hooks/blocking/CLAUDE.md +4 -1
  60. package/hooks/blocking/_path_setup.py +13 -0
  61. package/hooks/blocking/bash_pre_tool_use_dispatcher.py +281 -0
  62. package/hooks/blocking/code_rules_dead_module_constant.py +88 -16
  63. package/hooks/blocking/code_rules_enforcer.py +6 -0
  64. package/hooks/blocking/code_rules_imports_logging.py +34 -1
  65. package/hooks/blocking/code_rules_paths_syspath.py +1 -1
  66. package/hooks/blocking/code_rules_test_layout.py +272 -0
  67. package/hooks/blocking/nas_ssh_binary_enforcer.py +22 -12
  68. package/hooks/blocking/pii_prevention_blocker.py +781 -0
  69. package/hooks/blocking/pii_scanner.py +340 -0
  70. package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
  71. package/hooks/blocking/stop_dispatcher.py +84 -0
  72. package/hooks/blocking/test__path_setup.py +39 -0
  73. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +61 -0
  74. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +294 -0
  75. package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +1 -1
  76. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +51 -0
  77. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +103 -0
  78. package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +1 -1
  79. package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +1 -1
  80. package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +11 -11
  81. package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +1 -1
  82. package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +1 -1
  83. package/hooks/blocking/test_code_rules_enforcer_test_layout.py +111 -0
  84. package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +76 -0
  85. package/hooks/blocking/test_destructive_command_blocker.py +29 -9
  86. package/hooks/blocking/test_gh_body_arg_blocker.py +3 -3
  87. package/hooks/blocking/test_nas_ssh_binary_enforcer.py +96 -39
  88. package/hooks/blocking/test_pii_prevention_blocker.py +905 -0
  89. package/hooks/blocking/test_pii_prevention_windows_git_path.py +41 -0
  90. package/hooks/blocking/test_pii_scanner.py +165 -0
  91. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +1 -1
  92. package/hooks/blocking/test_pre_tool_use_dispatcher.py +14 -13
  93. package/hooks/blocking/test_stop_dispatcher.py +187 -0
  94. package/hooks/blocking/test_verdict_directory_write_blocker.py +1 -1
  95. package/hooks/blocking/test_volatile_path_in_post_blocker.py +3 -3
  96. package/hooks/blocking/volatile_path_in_post_blocker.py +54 -5
  97. package/hooks/diagnostic/migrations/README.md +25 -26
  98. package/hooks/diagnostic/test_hook_log_extractor.py +10 -10
  99. package/hooks/hooks.json +10 -120
  100. package/hooks/hooks_constants/CLAUDE.md +12 -3
  101. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +78 -0
  102. package/hooks/hooks_constants/dead_module_constant_constants.py +1 -0
  103. package/hooks/hooks_constants/hosted_hook_runner.py +73 -0
  104. package/hooks/hooks_constants/local_identity.py +182 -0
  105. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +4 -37
  106. package/hooks/hooks_constants/pii_prevention_constants.py +299 -0
  107. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  108. package/hooks/hooks_constants/python_style_checks_constants.py +15 -0
  109. package/hooks/hooks_constants/stop_dispatcher_constants.py +26 -0
  110. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +87 -0
  111. package/hooks/hooks_constants/test_hardcoded_user_path_constants.py +2 -2
  112. package/hooks/hooks_constants/test_hosted_hook_runner.py +101 -0
  113. package/hooks/hooks_constants/test_layout_constants.py +28 -0
  114. package/hooks/hooks_constants/test_local_identity.py +88 -0
  115. package/hooks/hooks_constants/test_session_env_cleanup_constants.py +1 -1
  116. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +32 -0
  117. package/hooks/validators/README.md +5 -6
  118. package/hooks/validators/__init__.py +2 -2
  119. package/hooks/validators/python_style_checks.py +298 -243
  120. package/hooks/validators/run_all_validators.py +6 -0
  121. package/hooks/validators/test_python_style_checks.py +278 -163
  122. package/package.json +2 -2
  123. package/rules/CLAUDE.md +2 -1
  124. package/rules/bdd.md +1 -1
  125. package/rules/nas-ssh-invocation.md +6 -4
  126. package/rules/no-justification-noise.md +61 -0
  127. package/scripts/test_setup_project_paths.py +1 -1
  128. package/skills/CLAUDE.md +6 -3
  129. package/skills/_shared/pr-loop/scripts/_path_resolver.py +2 -2
  130. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
  131. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
  132. package/skills/_shared/pr-loop/scripts/test__path_resolver.py +42 -3
  133. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +11 -11
  134. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +26 -3
  135. package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +49 -25
  136. package/skills/autoconverge/CLAUDE.md +3 -3
  137. package/skills/autoconverge/SKILL.md +113 -35
  138. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  139. package/skills/autoconverge/reference/convergence.md +44 -22
  140. package/skills/autoconverge/reference/gotchas.md +11 -0
  141. package/skills/autoconverge/reference/stop-conditions.md +23 -13
  142. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  143. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +15 -19
  144. package/skills/autoconverge/workflow/converge.contract.test.mjs +43 -42
  145. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +141 -16
  146. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +2 -2
  147. package/skills/autoconverge/workflow/converge.mjs +329 -68
  148. package/skills/autoconverge/workflow/converge.path-aware.test.mjs +1 -1
  149. package/skills/autoconverge/workflow/converge.precatch.test.mjs +152 -0
  150. package/skills/autoconverge/workflow/converge.run-input.test.mjs +3 -3
  151. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +15 -15
  152. package/skills/autoconverge/workflow/test_convergence_summary.py +6 -6
  153. package/skills/bdd-protocol/SKILL.md +4 -5
  154. package/skills/bdd-protocol/references/anti-patterns.md +1 -1
  155. package/skills/bugteam/SKILL.md +8 -0
  156. package/skills/bugteam/reference/copilot-gap-analysis.md +20 -20
  157. package/skills/bugteam/reference/obstacles/audit-walk-categories.md +1 -1
  158. package/skills/bugteam/reference/team-setup.md +7 -5
  159. package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
  160. package/skills/copilot-finding-triage/SKILL.md +124 -0
  161. package/skills/copilot-finding-triage/reference/tier-rubric.md +49 -0
  162. package/skills/copilot-finding-triage/scripts/conftest.py +8 -0
  163. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/__init__.py +0 -0
  164. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/__init__.py +0 -0
  165. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/notify_ntfy_constants.py +32 -0
  166. package/skills/copilot-finding-triage/scripts/notify_ntfy.py +192 -0
  167. package/skills/copilot-finding-triage/scripts/test_notify_ntfy.py +141 -0
  168. package/skills/copilot-finding-triage/templates/notification.md +34 -0
  169. package/skills/copilot-review/SKILL.md +8 -6
  170. package/skills/findbugs/SKILL.md +4 -0
  171. package/skills/fixbugs/SKILL.md +8 -7
  172. package/skills/gotcha/CLAUDE.md +2 -2
  173. package/skills/gotcha/SKILL.md +4 -4
  174. package/skills/log-audit/SKILL.md +4 -6
  175. package/skills/monitor-open-prs/CLAUDE.md +1 -1
  176. package/skills/monitor-open-prs/SKILL.md +6 -1
  177. package/skills/orchestrator/SKILL.md +74 -78
  178. package/skills/orchestrator-refresh/SKILL.md +24 -12
  179. package/skills/post-audit-findings/SKILL.md +5 -9
  180. package/skills/pr-consistency-audit/SKILL.md +5 -1
  181. package/skills/pr-converge/CLAUDE.md +1 -1
  182. package/skills/pr-converge/SKILL.md +86 -47
  183. package/skills/pr-converge/reference/CLAUDE.md +1 -1
  184. package/skills/pr-converge/reference/convergence-gates.md +20 -17
  185. package/skills/pr-converge/reference/examples.md +63 -47
  186. package/skills/pr-converge/reference/fix-protocol.md +15 -15
  187. package/skills/pr-converge/reference/ground-rules.md +11 -7
  188. package/skills/pr-converge/reference/multi-pr-orchestration.md +6 -5
  189. package/skills/pr-converge/reference/per-tick.md +129 -107
  190. package/skills/pr-converge/reference/state-schema.md +15 -10
  191. package/skills/pr-converge/scripts/check_convergence.py +5 -4
  192. package/skills/pr-converge/scripts/test_check_convergence.py +6 -4
  193. package/skills/pr-fix-protocol/SKILL.md +3 -8
  194. package/skills/pr-loop-cloud-transport/SKILL.md +102 -0
  195. package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +40 -0
  196. package/skills/pr-loop-cloud-transport/reference/substitution-matrix.md +48 -0
  197. package/skills/pr-loop-lifecycle/SKILL.md +5 -9
  198. package/skills/pr-scope-resolve/SKILL.md +3 -6
  199. package/skills/privacy-hygiene/SKILL.md +114 -0
  200. package/skills/qbug/SKILL.md +8 -8
  201. package/skills/rebase/SKILL.md +5 -1
  202. package/skills/refine/SKILL.md +4 -5
  203. package/skills/reviewer-gates/SKILL.md +7 -11
  204. package/skills/session-log/SKILL.md +4 -1
  205. package/skills/skill-builder/SKILL.md +3 -6
  206. package/skills/structure-prompt/SKILL.md +4 -5
  207. package/skills/team-advisor/SKILL.md +56 -0
  208. package/skills/test_markdown_link_integrity.py +10 -6
  209. package/skills/update/SKILL.md +5 -1
  210. package/skills/usage-pause/SKILL.md +14 -5
  211. package/skills/usage-pause/scripts/resolve_usage_window.py +83 -5
  212. package/skills/usage-pause/scripts/test_resolve_usage_window.py +185 -17
  213. package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +4 -3
  214. package/skills/verified-build/SKILL.md +4 -9
@@ -1,27 +1,26 @@
1
1
  """Tests for Python style checks."""
2
2
 
3
3
  import ast
4
- import tempfile
4
+ import os
5
+ import subprocess
6
+ import sys
5
7
  from pathlib import Path
6
- from typing import List
7
8
 
8
9
  import pytest
9
10
 
10
11
  from .python_style_checks import (
11
12
  Violation,
13
+ check_blank_lines_between_functions,
12
14
  check_imports_at_top,
13
15
  check_no_empty_line_after_decorators,
14
- check_single_empty_line_between_functions,
15
16
  check_view_function_naming,
16
17
  fix_file,
18
+ fix_function_spacing,
17
19
  validate_file,
18
20
  )
19
21
 
20
-
21
- # Test data: Code samples
22
22
  GOOD_IMPORTS = '''import os
23
23
  import sys
24
- from typing import List
25
24
 
26
25
  def foo() -> None:
27
26
  pass
@@ -38,6 +37,13 @@ BAD_IMPORTS_AFTER_CONSTANT = '''MY_CONSTANT = 42
38
37
  import os
39
38
  '''
40
39
 
40
+ ASYNC_BAD_INLINE_IMPORT = '''import asyncio
41
+
42
+ async def foo() -> None:
43
+ import json
44
+ await asyncio.sleep(0)
45
+ '''
46
+
41
47
  GOOD_DECORATOR_NO_BLANK = '''@decorator
42
48
  def foo() -> None:
43
49
  pass
@@ -49,9 +55,18 @@ def foo() -> None:
49
55
  pass
50
56
  '''
51
57
 
52
- GOOD_SINGLE_LINE_BETWEEN_FUNCTIONS = '''def foo() -> None:
58
+ ASYNC_BAD_DECORATOR_WITH_BLANK = '''import asyncio
59
+
60
+ @asyncio.coroutine
61
+
62
+ async def foo() -> None:
63
+ pass
64
+ '''
65
+
66
+ GOOD_TWO_LINES_BETWEEN_FUNCTIONS = '''def foo() -> None:
53
67
  pass
54
68
 
69
+
55
70
  def bar() -> None:
56
71
  pass
57
72
  '''
@@ -62,14 +77,30 @@ def bar() -> None:
62
77
  pass
63
78
  '''
64
79
 
65
- BAD_MULTIPLE_LINES_BETWEEN_FUNCTIONS = '''def foo() -> None:
80
+ BAD_ONE_LINE_BETWEEN_FUNCTIONS = '''def foo() -> None:
66
81
  pass
67
82
 
83
+ def bar() -> None:
84
+ pass
85
+ '''
86
+
87
+ BAD_THREE_LINES_BETWEEN_FUNCTIONS = '''def foo() -> None:
88
+ pass
89
+
90
+
68
91
 
69
92
  def bar() -> None:
70
93
  pass
71
94
  '''
72
95
 
96
+ ASYNC_GOOD_TWO_LINES_BETWEEN = '''async def foo() -> None:
97
+ pass
98
+
99
+
100
+ async def bar() -> None:
101
+ pass
102
+ '''
103
+
73
104
  GOOD_VIEW_NAMING = '''def user_profile_view(request):
74
105
  pass
75
106
 
@@ -80,77 +111,75 @@ def get_tasks_view(request, user_id):
80
111
  BAD_VIEW_NAMING = '''def user_profile(request):
81
112
  pass
82
113
 
83
- def getTasks(request, user_id):
114
+ def get_tasks(request, user_id):
84
115
  pass
85
116
  '''
86
117
 
87
- GOOD_NON_VIEW_FUNCTION = '''def helper_function(data):
118
+ GOOD_NON_VIEW_FUNCTION = '''def helper_function(payload):
88
119
  pass
89
120
 
90
- def process_data(items):
121
+ def process_records(items):
91
122
  pass
92
123
  '''
93
124
 
94
- # Async function test samples
95
- ASYNC_GOOD_DECORATOR_NO_BLANK = '''import asyncio
125
+ ASYNC_BAD_VIEW_NAMING = '''async def user_profile(request):
126
+ pass
96
127
 
97
- @asyncio.coroutine
98
- async def foo() -> None:
99
- await asyncio.sleep(0)
128
+ async def get_tasks(request, user_id):
129
+ pass
100
130
  '''
101
131
 
102
- ASYNC_BAD_DECORATOR_WITH_BLANK = '''import asyncio
132
+ DECORATED_NEXT_TWO_LINES = '''def foo() -> None:
133
+ pass
103
134
 
104
- @asyncio.coroutine
105
135
 
106
- async def foo() -> None:
107
- await asyncio.sleep(0)
136
+ @decorator
137
+ def bar() -> None:
138
+ pass
108
139
  '''
109
140
 
110
- ASYNC_GOOD_SINGLE_LINE_BETWEEN = '''import asyncio
111
-
112
- async def foo() -> None:
113
- await asyncio.sleep(0)
141
+ ONE_BLANK_BEFORE_DECORATED = '''def foo() -> None:
142
+ pass
114
143
 
115
- async def bar() -> None:
116
- await asyncio.sleep(0)
144
+ @decorator
145
+ def bar() -> None:
146
+ pass
117
147
  '''
118
148
 
119
- ASYNC_BAD_MULTIPLE_LINES_BETWEEN = '''import asyncio
120
-
121
- async def foo() -> None:
122
- await asyncio.sleep(0)
149
+ COMMENT_BETWEEN_FUNCTIONS = '''def foo() -> None:
150
+ pass
123
151
 
124
152
 
125
- async def bar() -> None:
126
- await asyncio.sleep(0)
153
+ # helper comment
154
+ def bar() -> None:
155
+ pass
127
156
  '''
128
157
 
129
- ASYNC_GOOD_VIEW_NAMING = '''from django.http import HttpRequest, HttpResponse
158
+ CLASS_BETWEEN_FUNCTIONS = '''def foo() -> None:
159
+ pass
160
+
161
+
162
+ class Middle:
163
+ pass
130
164
 
131
- async def user_profile_view(request: HttpRequest) -> HttpResponse:
132
- return HttpResponse("profile")
133
165
 
134
- async def get_tasks_view(request: HttpRequest, user_id: int) -> HttpResponse:
135
- return HttpResponse("tasks")
166
+ def bar() -> None:
167
+ pass
136
168
  '''
137
169
 
138
- ASYNC_BAD_VIEW_NAMING = '''from django.http import HttpRequest, HttpResponse
170
+ MODULE_STATEMENT_BETWEEN_FUNCTIONS = '''def foo() -> None:
171
+ pass
139
172
 
140
- async def user_profile(request: HttpRequest) -> HttpResponse:
141
- return HttpResponse("profile")
142
173
 
143
- async def getTasks(request: HttpRequest, user_id: int) -> HttpResponse:
144
- return HttpResponse("tasks")
145
- '''
174
+ answer = 42
146
175
 
147
- ASYNC_BAD_INLINE_IMPORT = '''import asyncio
148
176
 
149
- async def foo() -> None:
150
- import json
151
- await asyncio.sleep(0)
177
+ def bar() -> None:
178
+ pass
152
179
  '''
153
180
 
181
+ FORM_FEED_BETWEEN_FUNCTIONS = "def foo():\n pass\n\x0c\n\ndef bar():\n pass\n"
182
+
154
183
 
155
184
  class TestImportsAtTop:
156
185
  """Test import positioning validation."""
@@ -203,13 +232,6 @@ class TestNoEmptyLineAfterDecorators:
203
232
  assert violations[0].line == 1
204
233
  assert "decorator" in violations[0].message.lower()
205
234
 
206
- def test_async_no_blank_line_valid(self) -> None:
207
- """Decorator directly above async function should pass."""
208
- violations = check_no_empty_line_after_decorators(
209
- ASYNC_GOOD_DECORATOR_NO_BLANK, "test.py"
210
- )
211
- assert violations == []
212
-
213
235
  def test_async_blank_line_after_decorator_fails(self) -> None:
214
236
  """Blank line after decorator on async function should fail."""
215
237
  violations = check_no_empty_line_after_decorators(
@@ -220,46 +242,74 @@ class TestNoEmptyLineAfterDecorators:
220
242
  assert "decorator" in violations[0].message.lower()
221
243
 
222
244
 
223
- class TestSingleEmptyLineBetweenFunctions:
245
+ class TestBlankLinesBetweenFunctions:
224
246
  """Test function spacing validation."""
225
247
 
226
- def test_single_line_valid(self) -> None:
227
- """Exactly one blank line should pass."""
228
- violations = check_single_empty_line_between_functions(
229
- GOOD_SINGLE_LINE_BETWEEN_FUNCTIONS, "test.py"
248
+ def test_two_lines_valid(self) -> None:
249
+ """Exactly two blank lines should pass."""
250
+ violations = check_blank_lines_between_functions(
251
+ GOOD_TWO_LINES_BETWEEN_FUNCTIONS, "test.py"
230
252
  )
231
253
  assert violations == []
232
254
 
233
255
  def test_no_line_between_functions_fails(self) -> None:
234
256
  """No blank line should fail."""
235
- violations = check_single_empty_line_between_functions(
257
+ violations = check_blank_lines_between_functions(
236
258
  BAD_NO_LINE_BETWEEN_FUNCTIONS, "test.py"
237
259
  )
238
260
  assert len(violations) == 1
239
261
  assert "empty line" in violations[0].message.lower()
240
262
 
241
- def test_multiple_lines_between_functions_fails(self) -> None:
242
- """Multiple blank lines should fail."""
243
- violations = check_single_empty_line_between_functions(
244
- BAD_MULTIPLE_LINES_BETWEEN_FUNCTIONS, "test.py"
263
+ def test_one_line_between_functions_fails(self) -> None:
264
+ """A single blank line should fail under the two-line rule."""
265
+ violations = check_blank_lines_between_functions(
266
+ BAD_ONE_LINE_BETWEEN_FUNCTIONS, "test.py"
267
+ )
268
+ assert len(violations) == 1
269
+ assert "empty line" in violations[0].message.lower()
270
+
271
+ def test_three_lines_between_functions_fails(self) -> None:
272
+ """Three or more blank lines should fail."""
273
+ violations = check_blank_lines_between_functions(
274
+ BAD_THREE_LINES_BETWEEN_FUNCTIONS, "test.py"
245
275
  )
246
276
  assert len(violations) == 1
247
277
  assert "empty line" in violations[0].message.lower()
248
278
 
249
- def test_async_single_line_valid(self) -> None:
250
- """Exactly one blank line between async functions should pass."""
251
- violations = check_single_empty_line_between_functions(
252
- ASYNC_GOOD_SINGLE_LINE_BETWEEN, "test.py"
279
+ def test_async_two_lines_valid(self) -> None:
280
+ """Exactly two blank lines between async functions should pass."""
281
+ violations = check_blank_lines_between_functions(
282
+ ASYNC_GOOD_TWO_LINES_BETWEEN, "test.py"
253
283
  )
254
284
  assert violations == []
255
285
 
256
- def test_async_multiple_lines_fails(self) -> None:
257
- """Multiple blank lines between async functions should fail."""
258
- violations = check_single_empty_line_between_functions(
259
- ASYNC_BAD_MULTIPLE_LINES_BETWEEN, "test.py"
286
+ def test_decorated_next_function_two_lines_valid(self) -> None:
287
+ """Two blanks before a decorated next function should pass."""
288
+ violations = check_blank_lines_between_functions(
289
+ DECORATED_NEXT_TWO_LINES, "test.py"
260
290
  )
261
- assert len(violations) == 1
262
- assert "empty line" in violations[0].message.lower()
291
+ assert violations == []
292
+
293
+ def test_comment_between_functions_passes(self) -> None:
294
+ """A comment in the gap leaves the surrounding functions unflagged."""
295
+ violations = check_blank_lines_between_functions(
296
+ COMMENT_BETWEEN_FUNCTIONS, "test.py"
297
+ )
298
+ assert violations == []
299
+
300
+ def test_class_between_functions_passes(self) -> None:
301
+ """An interposed class leaves the surrounding functions unflagged."""
302
+ violations = check_blank_lines_between_functions(
303
+ CLASS_BETWEEN_FUNCTIONS, "test.py"
304
+ )
305
+ assert violations == []
306
+
307
+ def test_module_statement_between_functions_passes(self) -> None:
308
+ """A module-level statement in the gap leaves the pair unflagged."""
309
+ violations = check_blank_lines_between_functions(
310
+ MODULE_STATEMENT_BETWEEN_FUNCTIONS, "test.py"
311
+ )
312
+ assert violations == []
263
313
 
264
314
 
265
315
  class TestViewFunctionNaming:
@@ -276,7 +326,7 @@ class TestViewFunctionNaming:
276
326
  tree = ast.parse(BAD_VIEW_NAMING)
277
327
  violations = check_view_function_naming(tree, "views.py")
278
328
  assert len(violations) == 2
279
- assert all("_view" in v.message for v in violations)
329
+ assert all("_view" in each_violation.message for each_violation in violations)
280
330
 
281
331
  def test_non_view_file_ignored(self) -> None:
282
332
  """Non-views.py files should be ignored."""
@@ -290,63 +340,51 @@ class TestViewFunctionNaming:
290
340
  violations = check_view_function_naming(tree, "views.py")
291
341
  assert violations == []
292
342
 
293
- def test_async_view_functions_named_correctly(self) -> None:
294
- """Async view functions ending with _view should pass."""
295
- tree = ast.parse(ASYNC_GOOD_VIEW_NAMING)
296
- violations = check_view_function_naming(tree, "views.py")
297
- assert violations == []
298
-
299
343
  def test_async_view_functions_without_suffix_fail(self) -> None:
300
344
  """Async view functions not ending with _view should fail."""
301
345
  tree = ast.parse(ASYNC_BAD_VIEW_NAMING)
302
346
  violations = check_view_function_naming(tree, "views.py")
303
347
  assert len(violations) == 2
304
- assert all("_view" in v.message for v in violations)
348
+ assert all("_view" in each_violation.message for each_violation in violations)
305
349
 
306
350
 
307
351
  class TestValidateFile:
308
352
  """Test file validation integration."""
309
353
 
310
- def test_valid_file_passes(self) -> None:
354
+ def test_valid_file_passes(
355
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
356
+ ) -> None:
311
357
  """File with no violations should pass."""
312
- with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
313
- f.write(GOOD_IMPORTS)
314
- f.flush()
315
- temp_path = Path(f.name)
316
-
317
- try:
318
- violations = validate_file(temp_path)
319
- assert violations == []
320
- finally:
321
- temp_path.unlink()
322
-
323
- def test_invalid_file_returns_violations(self) -> None:
358
+ monkeypatch.setenv("HOME", str(tmp_path))
359
+ monkeypatch.setenv("TMPDIR", str(tmp_path))
360
+ temp_path = tmp_path / "valid_module.py"
361
+ temp_path.write_text(GOOD_IMPORTS, encoding="utf-8")
362
+ violations = validate_file(temp_path)
363
+ assert violations == []
364
+
365
+ def test_invalid_file_returns_violations(
366
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
367
+ ) -> None:
324
368
  """File with violations should return them."""
325
- with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
326
- f.write(BAD_IMPORTS_AFTER_CODE)
327
- f.flush()
328
- temp_path = Path(f.name)
329
-
330
- try:
331
- violations = validate_file(temp_path)
332
- assert len(violations) > 0
333
- assert all(isinstance(v, Violation) for v in violations)
334
- finally:
335
- temp_path.unlink()
336
-
337
- def test_syntax_error_returns_violation(self) -> None:
369
+ monkeypatch.setenv("HOME", str(tmp_path))
370
+ monkeypatch.setenv("TMPDIR", str(tmp_path))
371
+ temp_path = tmp_path / "invalid_module.py"
372
+ temp_path.write_text(BAD_IMPORTS_AFTER_CODE, encoding="utf-8")
373
+ violations = validate_file(temp_path)
374
+ assert len(violations) > 0
375
+ assert all(isinstance(each_violation, Violation) for each_violation in violations)
376
+
377
+ def test_syntax_error_returns_violation(
378
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
379
+ ) -> None:
338
380
  """File with syntax error should return violation."""
339
- with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
340
- f.write("def foo(\n") # Invalid syntax
341
- f.flush()
342
- temp_path = Path(f.name)
343
-
344
- try:
345
- violations = validate_file(temp_path)
346
- assert len(violations) == 1
347
- assert "syntax error" in violations[0].message.lower()
348
- finally:
349
- temp_path.unlink()
381
+ monkeypatch.setenv("HOME", str(tmp_path))
382
+ monkeypatch.setenv("TMPDIR", str(tmp_path))
383
+ temp_path = tmp_path / "broken_module.py"
384
+ temp_path.write_text("def foo(\n", encoding="utf-8")
385
+ violations = validate_file(temp_path)
386
+ assert len(violations) == 1
387
+ assert "syntax error" in violations[0].message.lower()
350
388
 
351
389
 
352
390
  class TestViolationClass:
@@ -354,22 +392,26 @@ class TestViolationClass:
354
392
 
355
393
  def test_violation_creation(self) -> None:
356
394
  """Violation should store file, line, message."""
357
- v = Violation("test.py", 42, "Test message")
358
- assert v.file == "test.py"
359
- assert v.line == 42
360
- assert v.message == "Test message"
395
+ violation = Violation("test.py", 42, "Test message")
396
+ assert violation.file == "test.py"
397
+ assert violation.line == 42
398
+ assert violation.message == "Test message"
361
399
 
362
400
  def test_violation_str_format(self) -> None:
363
401
  """Violation should format as file:line: message."""
364
- v = Violation("test.py", 42, "Test message")
365
- assert str(v) == "test.py:42: Test message"
402
+ violation = Violation("test.py", 42, "Test message")
403
+ assert str(violation) == "test.py:42: Test message"
366
404
 
367
405
 
368
406
  class TestAutoFix:
369
407
  """Test auto-fix capabilities."""
370
408
 
371
- def test_fix_empty_line_after_decorator(self) -> None:
409
+ def test_fix_empty_line_after_decorator(
410
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
411
+ ) -> None:
372
412
  """Auto-fix should remove blank line between decorator and function."""
413
+ monkeypatch.setenv("HOME", str(tmp_path))
414
+ monkeypatch.setenv("TMPDIR", str(tmp_path))
373
415
  code = '''@decorator
374
416
 
375
417
  def foo():
@@ -379,59 +421,132 @@ def foo():
379
421
  def foo():
380
422
  pass
381
423
  '''
382
- with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
383
- temp_file.write(code)
384
- temp_path = Path(temp_file.name)
385
-
386
- try:
387
- fixed = fix_file(temp_path)
388
- assert fixed is True
389
- result = temp_path.read_text()
390
- assert result.strip() == expected.strip()
391
- finally:
392
- temp_path.unlink()
393
-
394
- def test_fix_multiple_blank_lines_between_functions(self) -> None:
395
- """Auto-fix should collapse multiple blank lines to single."""
424
+ temp_path = tmp_path / "decorator_module.py"
425
+ temp_path.write_text(code, encoding="utf-8")
426
+ fixed = fix_file(temp_path)
427
+ assert fixed is True
428
+ result_text = temp_path.read_text()
429
+ assert result_text.strip() == expected.strip()
430
+
431
+ def test_fix_collapses_three_blank_lines_to_two(
432
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
433
+ ) -> None:
434
+ """Auto-fix should collapse three or more blank lines down to two."""
435
+ monkeypatch.setenv("HOME", str(tmp_path))
436
+ monkeypatch.setenv("TMPDIR", str(tmp_path))
396
437
  code = '''def foo():
397
438
  pass
398
439
 
399
440
 
441
+
400
442
  def bar():
401
443
  pass
402
444
  '''
403
445
  expected = '''def foo():
404
446
  pass
405
447
 
448
+
406
449
  def bar():
407
450
  pass
408
451
  '''
409
- with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
410
- temp_file.write(code)
411
- temp_path = Path(temp_file.name)
412
-
413
- try:
414
- fixed = fix_file(temp_path)
415
- assert fixed is True
416
- result = temp_path.read_text()
417
- assert result.strip() == expected.strip()
418
- finally:
419
- temp_path.unlink()
420
-
421
- def test_no_fix_needed_returns_false(self) -> None:
422
- """Auto-fix should return False when no fixes needed."""
452
+ temp_path = tmp_path / "spacing_module.py"
453
+ temp_path.write_text(code, encoding="utf-8")
454
+ fixed = fix_file(temp_path)
455
+ assert fixed is True
456
+ result_text = temp_path.read_text()
457
+ assert result_text.strip() == expected.strip()
458
+
459
+ def test_fix_inserts_missing_blank_lines(
460
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
461
+ ) -> None:
462
+ """Auto-fix should insert blank lines for under-spaced functions."""
463
+ monkeypatch.setenv("HOME", str(tmp_path))
464
+ monkeypatch.setenv("TMPDIR", str(tmp_path))
465
+ temp_path = tmp_path / "underspaced_module.py"
466
+ temp_path.write_text(BAD_ONE_LINE_BETWEEN_FUNCTIONS, encoding="utf-8")
467
+ fixed = fix_file(temp_path)
468
+ assert fixed is True
469
+ assert temp_path.read_text() == GOOD_TWO_LINES_BETWEEN_FUNCTIONS
470
+
471
+ def test_no_fix_needed_returns_false(
472
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
473
+ ) -> None:
474
+ """Auto-fix should return False when the file already uses two blank lines."""
475
+ monkeypatch.setenv("HOME", str(tmp_path))
476
+ monkeypatch.setenv("TMPDIR", str(tmp_path))
423
477
  code = '''def foo():
424
478
  pass
425
479
 
480
+
426
481
  def bar():
427
482
  pass
428
483
  '''
429
- with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
430
- temp_file.write(code)
431
- temp_path = Path(temp_file.name)
432
-
433
- try:
434
- fixed = fix_file(temp_path)
435
- assert fixed is False
436
- finally:
437
- temp_path.unlink()
484
+ temp_path = tmp_path / "clean_module.py"
485
+ temp_path.write_text(code, encoding="utf-8")
486
+ fixed = fix_file(temp_path)
487
+ assert fixed is False
488
+
489
+ def test_form_feed_gap_stays_check_clean_after_fix(
490
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
491
+ ) -> None:
492
+ """A form feed in a blank gap converges without corrupting the file."""
493
+ monkeypatch.setenv("HOME", str(tmp_path))
494
+ monkeypatch.setenv("TMPDIR", str(tmp_path))
495
+ assert (
496
+ check_blank_lines_between_functions(FORM_FEED_BETWEEN_FUNCTIONS, "test.py")
497
+ == []
498
+ )
499
+ temp_path = tmp_path / "form_feed_module.py"
500
+ temp_path.write_text(FORM_FEED_BETWEEN_FUNCTIONS, encoding="utf-8")
501
+ fix_file(temp_path)
502
+ fixed_text = temp_path.read_text()
503
+ assert check_blank_lines_between_functions(fixed_text, "test.py") == []
504
+ assert fix_file(temp_path) is False
505
+
506
+
507
+ class TestFixFunctionSpacing:
508
+ """Test blank-line normalization between top-level functions."""
509
+
510
+ def test_inserts_two_blank_lines_when_adjacent(self) -> None:
511
+ """Adjacent functions gain exactly two blank lines."""
512
+ fixed_source = fix_function_spacing(BAD_NO_LINE_BETWEEN_FUNCTIONS)
513
+ assert fixed_source == GOOD_TWO_LINES_BETWEEN_FUNCTIONS
514
+
515
+ def test_inserts_second_blank_line_when_one(self) -> None:
516
+ """A single blank line grows to exactly two."""
517
+ fixed_source = fix_function_spacing(BAD_ONE_LINE_BETWEEN_FUNCTIONS)
518
+ assert fixed_source == GOOD_TWO_LINES_BETWEEN_FUNCTIONS
519
+
520
+ def test_collapses_three_blank_lines_to_two(self) -> None:
521
+ """Three blank lines collapse to exactly two."""
522
+ fixed_source = fix_function_spacing(BAD_THREE_LINES_BETWEEN_FUNCTIONS)
523
+ assert fixed_source == GOOD_TWO_LINES_BETWEEN_FUNCTIONS
524
+
525
+ def test_normalizes_before_decorated_function(self) -> None:
526
+ """Blank lines before a decorated function normalize to two."""
527
+ fixed_source = fix_function_spacing(ONE_BLANK_BEFORE_DECORATED)
528
+ assert fixed_source == DECORATED_NEXT_TWO_LINES
529
+
530
+
531
+ class TestDirectInvocation:
532
+ """Test running the checker as a standalone script."""
533
+
534
+ def test_direct_invocation_resolves_hooks_constants(
535
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
536
+ ) -> None:
537
+ """Direct invocation bootstraps hooks_constants without PYTHONPATH."""
538
+ monkeypatch.setenv("HOME", str(tmp_path))
539
+ monkeypatch.setenv("TMPDIR", str(tmp_path))
540
+ script_path = Path(__file__).resolve().parent / "python_style_checks.py"
541
+ target_path = tmp_path / "clean_sample.py"
542
+ target_path.write_text(GOOD_IMPORTS, encoding="utf-8")
543
+ scrubbed_environment = dict(os.environ)
544
+ scrubbed_environment.pop("PYTHONPATH", None)
545
+ completed_process = subprocess.run(
546
+ [sys.executable, "-S", str(script_path), str(target_path)],
547
+ capture_output=True,
548
+ text=True,
549
+ env=scrubbed_environment,
550
+ )
551
+ assert "ModuleNotFoundError" not in completed_process.stderr
552
+ assert completed_process.returncode == 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.92.1",
3
+ "version": "1.93.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,7 +36,7 @@
36
36
  "license": "MIT",
37
37
  "repository": {
38
38
  "type": "git",
39
- "url": "git+https://github.com/jl-cmd/claude-code-config.git",
39
+ "url": "git+https://github.com/jl-cmd/claude-dev-env.git",
40
40
  "directory": "packages/claude-dev-env"
41
41
  }
42
42
  }
package/rules/CLAUDE.md CHANGED
@@ -30,10 +30,11 @@ Rule files installed into `~/.claude/rules/` by `bin/install.mjs`. Claude Code l
30
30
  | `git-workflow.md` | PR workflow: always create as draft, one commit per review stage, never commit working docs or images |
31
31
  | `hook-prose-matches-detector.md` | Hook prose descriptions match what the hook actually detects |
32
32
  | `long-horizon-autonomy.md` | Autonomous-run behaviors: act on what you have, do not end on a promise, delegate and keep working |
33
- | `nas-ssh-invocation.md` | Reach the NAS at `192.168.1.100` through the `System32/OpenSSH` binary with `-o BatchMode=yes`; bare `ssh`/`scp`/`sftp` stalls on an interactive password prompt |
33
+ | `nas-ssh-invocation.md` | Reach the NAS through the `System32/OpenSSH` binary with `-o BatchMode=yes`; bare `ssh`/`scp`/`sftp` stalls on an interactive password prompt |
34
34
  | `no-cross-skill-duplicate-helpers.md` | No duplicating shared helpers across skills; use `_shared/` |
35
35
  | `no-historical-clutter.md` | Documentation describes current state only; no historical or transitional language |
36
36
  | `no-inline-destructive-literals.md` | No destructive-command literals in Bash tool command strings, even as data |
37
+ | `no-justification-noise.md` | Markdown states facts a reader can act on; cut a present-tense sentence that only justifies a stated choice or restates a gain the reader already works out from the behavior or from a rule enforced elsewhere |
37
38
  | `env-var-table-code-drift.md` | Every env-var summary table row in a `.md` file names a code file whose source references the variable |
38
39
  | `es-exe-file-search.md` | File search on Windows routes through the `es.exe` CLI with a scoped query; the Everything HTTP server stays off |
39
40
  | `orphan-css-class.md` | Every `class="..."` attribute in Python-generated markup has a matching selector in the `<style>` block |
package/rules/bdd.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Canonical detail:** `~/.claude/system-prompts/software-engineer.xml` → `<behavior_protocol>`.
4
4
 
5
- **On-demand depth:** `@~/.claude/skills/bdd-protocol/SKILL.md` (Example Mapping §6.4, §7.6 catalog, solo patterns). Tracking design: [jl-cmd/claude-code-config#82](https://github.com/jl-cmd/claude-code-config/issues/82).
5
+ **On-demand depth:** `@~/.claude/skills/bdd-protocol/SKILL.md` (Example Mapping §6.4, §7.6 catalog, solo patterns). Tracking design: [jl-cmd/claude-dev-env#82](https://github.com/jl-cmd/claude-dev-env/issues/82).
6
6
 
7
7
  **Optional long-form references (load when needed):**
8
8