claude-dev-env 1.67.2 → 1.69.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 (124) hide show
  1. package/_shared/CLAUDE.md +13 -0
  2. package/_shared/pr-loop/CLAUDE.md +24 -0
  3. package/_shared/pr-loop/scripts/CLAUDE.md +30 -0
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +21 -0
  5. package/_shared/pr-loop/scripts/tests/CLAUDE.md +32 -0
  6. package/agents/CLAUDE.md +29 -0
  7. package/audit-rubrics/CLAUDE.md +41 -0
  8. package/audit-rubrics/category_rubrics/CLAUDE.md +36 -0
  9. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  10. package/audit-rubrics/prompts/CLAUDE.md +36 -0
  11. package/bin/CLAUDE.md +28 -0
  12. package/commands/CLAUDE.md +25 -0
  13. package/docs/CLAUDE.md +28 -0
  14. package/docs/references/CLAUDE.md +13 -0
  15. package/hooks/CLAUDE.md +31 -0
  16. package/hooks/advisory/CLAUDE.md +16 -0
  17. package/hooks/blocking/CLAUDE.md +107 -0
  18. package/hooks/blocking/code_rules_constants_config.py +7 -4
  19. package/hooks/blocking/code_rules_docstrings.py +253 -0
  20. package/hooks/blocking/code_rules_enforcer.py +6 -0
  21. package/hooks/blocking/config/CLAUDE.md +22 -0
  22. package/hooks/blocking/test_code_rules_enforcer_class_docstring_methods.py +262 -0
  23. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +36 -0
  24. package/hooks/blocking/test_code_rules_enforcer_docstring_fallback_branch.py +398 -0
  25. package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +9 -0
  26. package/hooks/diagnostic/CLAUDE.md +43 -0
  27. package/hooks/diagnostic/migrations/CLAUDE.md +16 -0
  28. package/hooks/diagnostic/queries/CLAUDE.md +19 -0
  29. package/hooks/git-hooks/CLAUDE.md +28 -0
  30. package/hooks/git-hooks/git_hooks_constants/CLAUDE.md +21 -0
  31. package/hooks/hooks_constants/CLAUDE.md +60 -0
  32. package/hooks/hooks_constants/blocking_check_limits.py +18 -0
  33. package/hooks/lifecycle/CLAUDE.md +18 -0
  34. package/hooks/observability/CLAUDE.md +16 -0
  35. package/hooks/session/CLAUDE.md +21 -0
  36. package/hooks/validation/CLAUDE.md +19 -0
  37. package/hooks/validators/CLAUDE.md +49 -0
  38. package/hooks/workflow/CLAUDE.md +22 -0
  39. package/package.json +1 -1
  40. package/rules/CLAUDE.md +46 -0
  41. package/rules/docstring-prose-matches-implementation.md +3 -2
  42. package/scripts/CLAUDE.md +34 -0
  43. package/scripts/dev_env_scripts_constants/CLAUDE.md +14 -0
  44. package/scripts/sync_to_cursor/CLAUDE.md +23 -0
  45. package/scripts/tests/CLAUDE.md +18 -0
  46. package/skills/CLAUDE.md +66 -0
  47. package/skills/_shared/CLAUDE.md +11 -0
  48. package/skills/_shared/pr-loop/CLAUDE.md +27 -0
  49. package/skills/_shared/pr-loop/prompts/CLAUDE.md +9 -0
  50. package/skills/_shared/pr-loop/scripts/CLAUDE.md +23 -0
  51. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +19 -0
  52. package/skills/anthropic-plan/CLAUDE.md +34 -0
  53. package/skills/anthropic-plan/SKILL.md +1 -1
  54. package/skills/anthropic-plan/scripts/CLAUDE.md +11 -0
  55. package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/CLAUDE.md +16 -0
  56. package/skills/anthropic-plan/templates/CLAUDE.md +13 -0
  57. package/skills/anthropic-plan/workflow/CLAUDE.md +14 -0
  58. package/skills/auditing-claude-config/CLAUDE.md +20 -0
  59. package/skills/autoconverge/CLAUDE.md +30 -0
  60. package/skills/autoconverge/reference/CLAUDE.md +12 -0
  61. package/skills/autoconverge/workflow/CLAUDE.md +23 -0
  62. package/skills/autoconverge/workflow/autoconverge_report_constants/CLAUDE.md +16 -0
  63. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +135 -0
  64. package/skills/autoconverge/workflow/converge.mjs +119 -2
  65. package/skills/bdd-protocol/CLAUDE.md +26 -0
  66. package/skills/bdd-protocol/references/CLAUDE.md +10 -0
  67. package/skills/bg-agent/CLAUDE.md +17 -0
  68. package/skills/bugteam/CLAUDE.md +30 -0
  69. package/skills/bugteam/reference/CLAUDE.md +22 -0
  70. package/skills/bugteam/reference/obstacles/CLAUDE.md +24 -0
  71. package/skills/bugteam/scripts/CLAUDE.md +36 -0
  72. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +20 -0
  73. package/skills/caveman/CLAUDE.md +15 -0
  74. package/skills/code/CLAUDE.md +17 -0
  75. package/skills/copilot-review/CLAUDE.md +17 -0
  76. package/skills/deep-research/CLAUDE.md +17 -0
  77. package/skills/doc-gist/CLAUDE.md +25 -0
  78. package/skills/doc-gist/references/CLAUDE.md +9 -0
  79. package/skills/doc-gist/references/examples/CLAUDE.md +25 -0
  80. package/skills/doc-gist/scripts/CLAUDE.md +27 -0
  81. package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +10 -0
  82. package/skills/everything-search/CLAUDE.md +17 -0
  83. package/skills/findbugs/CLAUDE.md +20 -0
  84. package/skills/fixbugs/CLAUDE.md +19 -0
  85. package/skills/fresh-branch/CLAUDE.md +15 -0
  86. package/skills/gh-paginate/CLAUDE.md +18 -0
  87. package/skills/gotcha/CLAUDE.md +33 -0
  88. package/skills/implement/CLAUDE.md +27 -0
  89. package/skills/implement/scripts/CLAUDE.md +22 -0
  90. package/skills/implement/scripts/implement_scripts_constants/CLAUDE.md +22 -0
  91. package/skills/logifix/CLAUDE.md +36 -0
  92. package/skills/logifix/scripts/CLAUDE.md +16 -0
  93. package/skills/monitor-open-prs/CLAUDE.md +34 -0
  94. package/skills/monitor-open-prs/scripts/CLAUDE.md +17 -0
  95. package/skills/pr-consistency-audit/CLAUDE.md +34 -0
  96. package/skills/pr-consistency-audit/reference/CLAUDE.md +16 -0
  97. package/skills/pr-converge/CLAUDE.md +29 -0
  98. package/skills/pr-converge/pr_converge_skill_constants/CLAUDE.md +26 -0
  99. package/skills/pr-converge/reference/CLAUDE.md +27 -0
  100. package/skills/pr-converge/reference/obstacles/CLAUDE.md +23 -0
  101. package/skills/pr-converge/scripts/CLAUDE.md +36 -0
  102. package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +17 -0
  103. package/skills/pr-converge/workflows/CLAUDE.md +16 -0
  104. package/skills/pr-review-responder/CLAUDE.md +35 -0
  105. package/skills/pre-compact/CLAUDE.md +24 -0
  106. package/skills/qbug/CLAUDE.md +40 -0
  107. package/skills/rebase/CLAUDE.md +32 -0
  108. package/skills/recall/CLAUDE.md +30 -0
  109. package/skills/refine/CLAUDE.md +44 -0
  110. package/skills/refine/templates/CLAUDE.md +17 -0
  111. package/skills/remember/CLAUDE.md +31 -0
  112. package/skills/research-mode/CLAUDE.md +35 -0
  113. package/skills/session-log/CLAUDE.md +31 -0
  114. package/skills/session-tidy/CLAUDE.md +36 -0
  115. package/skills/skill-builder/CLAUDE.md +45 -0
  116. package/skills/skill-builder/references/CLAUDE.md +19 -0
  117. package/skills/skill-builder/templates/CLAUDE.md +14 -0
  118. package/skills/skill-builder/workflows/CLAUDE.md +17 -0
  119. package/skills/structure-prompt/CLAUDE.md +42 -0
  120. package/skills/structure-prompt/reference/CLAUDE.md +28 -0
  121. package/skills/task-build/CLAUDE.md +28 -0
  122. package/skills/update/CLAUDE.md +38 -0
  123. package/skills/verified-build/CLAUDE.md +33 -0
  124. package/system-prompts/CLAUDE.md +17 -0
@@ -0,0 +1,398 @@
1
+ """Tests for check_docstring_fallback_branch_coverage — O6 fallback-branch drift.
2
+
3
+ A function whose summary scopes a fallback to one condition while the body
4
+ routes to that same fallback call from two or more distinct early-return guards
5
+ hides the second condition from the reader. This is the deterministic slice of
6
+ Category O6 (docstring prose vs implementation drift).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib.util
12
+ from pathlib import Path
13
+ from types import ModuleType
14
+
15
+
16
+ def _load_enforcer_module() -> ModuleType:
17
+ module_path = Path(__file__).parent / "code_rules_enforcer.py"
18
+ spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
19
+ assert spec is not None
20
+ assert spec.loader is not None
21
+ module = importlib.util.module_from_spec(spec)
22
+ spec.loader.exec_module(module)
23
+ return module
24
+
25
+
26
+ code_rules_enforcer = _load_enforcer_module()
27
+
28
+
29
+ def check_docstring_fallback_branch_coverage(content: str, file_path: str) -> list[str]:
30
+ return code_rules_enforcer.check_docstring_fallback_branch_coverage(content, file_path)
31
+
32
+
33
+ def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
34
+ return code_rules_enforcer.validate_content(content, file_path, old_content)
35
+
36
+
37
+ PRODUCTION_FILE_PATH = "/project/src/human_actions.py"
38
+ TEST_FILE_PATH = "/project/src/test_human_actions.py"
39
+ HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
40
+
41
+
42
+ def _drifted_scroll_method() -> str:
43
+ return (
44
+ "class HumanActions:\n"
45
+ " async def _scroll_once_toward_target(\n"
46
+ " self, all_container_screen_bounds: object\n"
47
+ " ) -> None:\n"
48
+ ' """Drive one scrollbar pass, falling back to the keyboard when'
49
+ ' the bar has no geometry."""\n'
50
+ " if all_container_screen_bounds is None:\n"
51
+ " await self._activate_then_press_right_arrow(None)\n"
52
+ " return\n"
53
+ " if random.random() < wheel_scroll_config.keyboard_scroll_fallback_probability:\n"
54
+ " await self._activate_then_press_right_arrow(all_container_screen_bounds)\n"
55
+ " return\n"
56
+ " await self._drive_scrollbar_gesture(all_container_screen_bounds)\n"
57
+ )
58
+
59
+
60
+ def _enumerated_scroll_method() -> str:
61
+ return (
62
+ "class HumanActions:\n"
63
+ " async def _scroll_once_toward_target(\n"
64
+ " self, all_container_screen_bounds: object\n"
65
+ " ) -> None:\n"
66
+ ' """Drive one scrollbar pass.\n'
67
+ "\n"
68
+ " Route to the Right-Arrow keyboard burst either when the bar has\n"
69
+ " no geometry or, on a random keyboard_scroll_fallback_probability\n"
70
+ " fraction of passes, when geometry is available.\n"
71
+ ' """\n'
72
+ " if all_container_screen_bounds is None:\n"
73
+ " await self._activate_then_press_right_arrow(None)\n"
74
+ " return\n"
75
+ " if random.random() < wheel_scroll_config.keyboard_scroll_fallback_probability:\n"
76
+ " await self._activate_then_press_right_arrow(all_container_screen_bounds)\n"
77
+ " return\n"
78
+ " await self._drive_scrollbar_gesture(all_container_screen_bounds)\n"
79
+ )
80
+
81
+
82
+ def test_should_flag_two_branches_routing_to_one_scoped_fallback() -> None:
83
+ issues = check_docstring_fallback_branch_coverage(
84
+ _drifted_scroll_method(), PRODUCTION_FILE_PATH
85
+ )
86
+ assert any("_activate_then_press_right_arrow" in each for each in issues), (
87
+ f"Expected the second fallback route to be flagged, got: {issues!r}"
88
+ )
89
+ assert len(issues) == 1
90
+
91
+
92
+ def test_should_report_the_route_count_in_the_message() -> None:
93
+ issues = check_docstring_fallback_branch_coverage(
94
+ _drifted_scroll_method(), PRODUCTION_FILE_PATH
95
+ )
96
+ assert any("2 distinct branches" in each for each in issues), (
97
+ f"Expected the branch count in the message, got: {issues!r}"
98
+ )
99
+
100
+
101
+ def test_should_not_flag_when_both_conditions_are_enumerated() -> None:
102
+ issues = check_docstring_fallback_branch_coverage(
103
+ _enumerated_scroll_method(), PRODUCTION_FILE_PATH
104
+ )
105
+ assert issues == [], (
106
+ f"A docstring that enumerates both routes must not be flagged, got: {issues!r}"
107
+ )
108
+
109
+
110
+ def test_should_not_flag_single_branch_fallback() -> None:
111
+ source = (
112
+ "def render(view: object) -> str:\n"
113
+ ' """Render the view, falling back to the empty string when absent."""\n'
114
+ " if view is None:\n"
115
+ " return ''\n"
116
+ " return view.body\n"
117
+ )
118
+ issues = check_docstring_fallback_branch_coverage(source, PRODUCTION_FILE_PATH)
119
+ assert issues == [], f"One fallback route under one named condition is correct, got: {issues!r}"
120
+
121
+
122
+ def test_should_not_flag_two_branches_to_different_callees() -> None:
123
+ source = (
124
+ "def dispatch(event: object) -> None:\n"
125
+ ' """Dispatch the event, falling back to the logger when unroutable."""\n'
126
+ " if event is None:\n"
127
+ " log_warning('empty')\n"
128
+ " return\n"
129
+ " if event.is_stale:\n"
130
+ " drop_event(event)\n"
131
+ " return\n"
132
+ " route_event(event)\n"
133
+ )
134
+ issues = check_docstring_fallback_branch_coverage(source, PRODUCTION_FILE_PATH)
135
+ assert issues == [], (
136
+ f"Distinct callees per branch are not a shared-fallback drift, got: {issues!r}"
137
+ )
138
+
139
+
140
+ def test_should_not_flag_when_docstring_has_no_scope_phrase() -> None:
141
+ source = (
142
+ "def select(target: object) -> None:\n"
143
+ ' """Pick the first matching candidate from the registry."""\n'
144
+ " if target is None:\n"
145
+ " await _press(None)\n"
146
+ " return\n"
147
+ " if target.is_idle:\n"
148
+ " await _press(target)\n"
149
+ " return\n"
150
+ " await _drive(target)\n"
151
+ )
152
+ issues = check_docstring_fallback_branch_coverage(source, PRODUCTION_FILE_PATH)
153
+ assert issues == [], (
154
+ f"No exclusive-scope phrase means no fallback claim to check, got: {issues!r}"
155
+ )
156
+
157
+
158
+ def test_should_not_flag_when_scope_phrase_is_a_substring_of_another_word() -> None:
159
+ source = (
160
+ "def refresh(cache: object) -> None:\n"
161
+ ' """Rebuild the cache; commonly when idle it reuses the warm copy."""\n'
162
+ " if cache is None:\n"
163
+ " rebuild_cache(None)\n"
164
+ " return\n"
165
+ " if cache.is_cold:\n"
166
+ " rebuild_cache(cache)\n"
167
+ " return\n"
168
+ " serve_cache(cache)\n"
169
+ )
170
+ issues = check_docstring_fallback_branch_coverage(source, PRODUCTION_FILE_PATH)
171
+ assert issues == [], (
172
+ "'commonly when' must not match the 'only when' scope phrase as a bare "
173
+ f"substring, got: {issues!r}"
174
+ )
175
+
176
+
177
+ def test_should_still_flag_word_boundary_scope_phrase() -> None:
178
+ source = (
179
+ "def refresh(cache: object) -> None:\n"
180
+ ' """Rebuild the cache only when it is invalid."""\n'
181
+ " if cache is None:\n"
182
+ " rebuild_cache(None)\n"
183
+ " return\n"
184
+ " if cache.is_cold:\n"
185
+ " rebuild_cache(cache)\n"
186
+ " return\n"
187
+ " serve_cache(cache)\n"
188
+ )
189
+ issues = check_docstring_fallback_branch_coverage(source, PRODUCTION_FILE_PATH)
190
+ assert any("rebuild_cache" in each for each in issues), (
191
+ f"A genuine 'only when' scope phrase must still be flagged, got: {issues!r}"
192
+ )
193
+
194
+
195
+ def test_should_not_flag_when_summary_enumerates_both_conditions_inline() -> None:
196
+ source = (
197
+ "def scroll(bar: object) -> None:\n"
198
+ ' """Drive a scrollbar pass, falling back to the keyboard when the bar'
199
+ ' lacks geometry or on a random fraction of passes."""\n'
200
+ " if bar is None:\n"
201
+ " _keyboard(None)\n"
202
+ " return\n"
203
+ " if bar.is_random:\n"
204
+ " _keyboard(bar)\n"
205
+ " return\n"
206
+ " _drive(bar)\n"
207
+ )
208
+ issues = check_docstring_fallback_branch_coverage(source, PRODUCTION_FILE_PATH)
209
+ assert issues == [], (
210
+ "A summary that enumerates both fallback conditions inline with 'or' is not "
211
+ f"a single-condition scope and must not be flagged, got: {issues!r}"
212
+ )
213
+
214
+
215
+ def test_should_still_flag_single_condition_fallback_with_two_routes() -> None:
216
+ source = (
217
+ "def scroll(bar: object) -> None:\n"
218
+ ' """Drive a scrollbar pass, falling back to the keyboard when the bar'
219
+ ' lacks geometry."""\n'
220
+ " if bar is None:\n"
221
+ " _keyboard(None)\n"
222
+ " return\n"
223
+ " if bar.is_random:\n"
224
+ " _keyboard(bar)\n"
225
+ " return\n"
226
+ " _drive(bar)\n"
227
+ )
228
+ issues = check_docstring_fallback_branch_coverage(source, PRODUCTION_FILE_PATH)
229
+ assert any("_keyboard" in each for each in issues), (
230
+ "A summary scoping the fallback to one named condition while two routes reach "
231
+ f"it must still be flagged, got: {issues!r}"
232
+ )
233
+
234
+
235
+ def test_should_not_flag_when_scope_phrase_is_a_left_anchored_prefix() -> None:
236
+ source = (
237
+ "def forward(packet: object) -> None:\n"
238
+ ' """Forward the packet; falls back toward the default sink when both'
239
+ ' checks miss."""\n'
240
+ " if packet is None:\n"
241
+ " send_to_sink(None)\n"
242
+ " return\n"
243
+ " if packet.is_stale:\n"
244
+ " send_to_sink(packet)\n"
245
+ " return\n"
246
+ " deliver(packet)\n"
247
+ )
248
+ issues = check_docstring_fallback_branch_coverage(source, PRODUCTION_FILE_PATH)
249
+ assert issues == [], (
250
+ "'falls back toward' must not match the 'falls back to' scope phrase as a "
251
+ f"left-anchored prefix, got: {issues!r}"
252
+ )
253
+
254
+
255
+ def test_should_not_flag_only_whenever_left_anchored_prefix() -> None:
256
+ source = (
257
+ "def refresh(cache: object) -> None:\n"
258
+ ' """Rebuild the cache only whenever it is invalid or stale."""\n'
259
+ " if cache is None:\n"
260
+ " rebuild_cache(None)\n"
261
+ " return\n"
262
+ " if cache.is_cold:\n"
263
+ " rebuild_cache(cache)\n"
264
+ " return\n"
265
+ " serve_cache(cache)\n"
266
+ )
267
+ issues = check_docstring_fallback_branch_coverage(source, PRODUCTION_FILE_PATH)
268
+ assert issues == [], (
269
+ "'only whenever' must not match the 'only when' scope phrase as a "
270
+ f"left-anchored prefix, got: {issues!r}"
271
+ )
272
+
273
+
274
+ def test_should_not_flag_two_branches_to_same_method_on_distinct_indexed_receivers() -> None:
275
+ source = (
276
+ "class Pool:\n"
277
+ " def shutdown(self, signal: object) -> None:\n"
278
+ ' """Close resources only when a shutdown signal arrives."""\n'
279
+ " if signal is None:\n"
280
+ " self.pool[0].close(signal)\n"
281
+ " return\n"
282
+ " if signal.is_secondary:\n"
283
+ " self.pool[1].close(signal)\n"
284
+ " return\n"
285
+ " self.pool[2].drain(signal)\n"
286
+ )
287
+ issues = check_docstring_fallback_branch_coverage(source, PRODUCTION_FILE_PATH)
288
+ assert issues == [], (
289
+ "Distinct indexed receivers calling the same method name are different "
290
+ f"fallbacks, got: {issues!r}"
291
+ )
292
+
293
+
294
+ def test_should_not_flag_two_branches_to_same_named_method_on_distinct_receivers() -> None:
295
+ source = (
296
+ "class Closer:\n"
297
+ " def shutdown(self, signal: object) -> None:\n"
298
+ ' """Close resources only when a shutdown signal arrives."""\n'
299
+ " if signal is None:\n"
300
+ " self.primary.close(signal)\n"
301
+ " return\n"
302
+ " if signal.is_secondary:\n"
303
+ " self.secondary.close(signal)\n"
304
+ " return\n"
305
+ " self.tertiary.close(signal)\n"
306
+ )
307
+ issues = check_docstring_fallback_branch_coverage(source, PRODUCTION_FILE_PATH)
308
+ assert issues == [], (
309
+ "Distinct receivers calling the same method name are different fallbacks, "
310
+ f"got: {issues!r}"
311
+ )
312
+
313
+
314
+ def test_should_flag_two_branches_to_same_method_on_one_receiver() -> None:
315
+ source = (
316
+ "class Closer:\n"
317
+ " def shutdown(self, signal: object) -> None:\n"
318
+ ' """Close resources only when a shutdown signal arrives."""\n'
319
+ " if signal is None:\n"
320
+ " self.primary.close(signal)\n"
321
+ " return\n"
322
+ " if signal.is_secondary:\n"
323
+ " self.primary.close(signal)\n"
324
+ " return\n"
325
+ " self.primary.drain(signal)\n"
326
+ )
327
+ issues = check_docstring_fallback_branch_coverage(source, PRODUCTION_FILE_PATH)
328
+ assert any("self.primary.close" in each for each in issues), (
329
+ f"Two routes to the same receiver.method must be flagged, got: {issues!r}"
330
+ )
331
+
332
+
333
+ def test_should_flag_multi_statement_guard_with_one_call_before_return() -> None:
334
+ source = (
335
+ "def select(target: object) -> None:\n"
336
+ ' """Pick a candidate, falling back to the press action when idle."""\n'
337
+ " if target is None:\n"
338
+ " attempt = 1\n"
339
+ " _press(None)\n"
340
+ " return\n"
341
+ " if target.is_idle:\n"
342
+ " attempt = 1\n"
343
+ " _press(target)\n"
344
+ " return\n"
345
+ " _drive(target)\n"
346
+ )
347
+ issues = check_docstring_fallback_branch_coverage(source, PRODUCTION_FILE_PATH)
348
+ assert any("_press" in each for each in issues), (
349
+ "A guard with a non-call statement before its single call still routes "
350
+ f"to that call, got: {issues!r}"
351
+ )
352
+
353
+
354
+ def test_should_not_flag_guard_with_a_second_call_expression() -> None:
355
+ source = (
356
+ "def select(target: object) -> None:\n"
357
+ ' """Pick a candidate, falling back to the press action when idle."""\n'
358
+ " if target is None:\n"
359
+ " _press(None)\n"
360
+ " _press(None)\n"
361
+ " return\n"
362
+ " if target.is_idle:\n"
363
+ " _press(target)\n"
364
+ " _press(target)\n"
365
+ " return\n"
366
+ " _drive(target)\n"
367
+ )
368
+ issues = check_docstring_fallback_branch_coverage(source, PRODUCTION_FILE_PATH)
369
+ assert issues == [], (
370
+ f"A second call expression disqualifies the block as a route, got: {issues!r}"
371
+ )
372
+
373
+
374
+ def test_should_skip_test_file() -> None:
375
+ issues = check_docstring_fallback_branch_coverage(_drifted_scroll_method(), TEST_FILE_PATH)
376
+ assert issues == [], f"Test files exempt, got: {issues!r}"
377
+
378
+
379
+ def test_should_skip_hook_infrastructure() -> None:
380
+ issues = check_docstring_fallback_branch_coverage(
381
+ _drifted_scroll_method(), HOOK_INFRASTRUCTURE_PATH
382
+ )
383
+ assert issues == [], f"Hook infrastructure exempt, got: {issues!r}"
384
+
385
+
386
+ def test_should_handle_syntax_error_gracefully() -> None:
387
+ issues = check_docstring_fallback_branch_coverage("def fetch(\n", PRODUCTION_FILE_PATH)
388
+ assert issues == [], f"Syntax error must yield no issues, got: {issues!r}"
389
+
390
+
391
+ def test_validate_content_surfaces_fallback_branch_drift() -> None:
392
+ issues = validate_content(_drifted_scroll_method(), PRODUCTION_FILE_PATH, old_content="")
393
+ matching_issues = [
394
+ each for each in issues if "_activate_then_press_right_arrow" in each and "O6" in each
395
+ ]
396
+ assert matching_issues, (
397
+ f"Expected validate_content to surface the O6 fallback-branch drift, got: {issues!r}"
398
+ )
@@ -32,6 +32,7 @@ TYPESCRIPT_FILE_PATH = "packages/claude-dev-env/hooks/blocking/example.ts"
32
32
  TOP_LEVEL_CONFIG_FILE_PATH = "config/timing.py"
33
33
  NESTED_CONFIG_FILE_PATH = "packages/claude-dev-env/hooks/config/example_constants.py"
34
34
  BACKSLASH_CONFIG_FILE_PATH = "packages\\claude-dev-env\\hooks\\config\\example_constants.py"
35
+ WORKFLOW_REGISTRY_FILE_PATH = "packages/claude-dev-env/hooks/blocking/workflow/app_info/states.py"
35
36
 
36
37
 
37
38
  def test_should_flag_constant_used_by_only_one_function() -> None:
@@ -114,6 +115,14 @@ def test_should_exempt_test_files() -> None:
114
115
  assert issues == [], f"Expected test file exemption, got: {issues}"
115
116
 
116
117
 
118
+ def test_should_exempt_workflow_registry_files() -> None:
119
+ source = "UPPER = 1\n\ndef lonely_caller():\n return UPPER\n"
120
+ issues = code_rules_enforcer.check_file_global_constants_use_count(
121
+ source, WORKFLOW_REGISTRY_FILE_PATH
122
+ )
123
+ assert issues == [], f"Expected workflow-registry file exemption, got: {issues}"
124
+
125
+
117
126
  def test_should_flag_constant_used_only_in_decorator_of_one_function() -> None:
118
127
  source = (
119
128
  "TIMEOUT = 5.0\n"
@@ -0,0 +1,43 @@
1
+ # hooks/diagnostic
2
+
3
+ Hooks and scripts that collect, store, and query hook-firing records. The pipeline reads session JSONL transcripts, extracts hook attachment records, and writes them as rows into a Neon (Postgres) `hook_events` table.
4
+
5
+ ## Subdirectories
6
+
7
+ | Directory | Role |
8
+ |---|---|
9
+ | `migrations/` | SQL migration files for the `hook_events` schema |
10
+ | `queries/` | Parameterized SQL queries for inspecting blocked commands |
11
+
12
+ ## Key files
13
+
14
+ | File | What it does |
15
+ |---|---|
16
+ | `hook_log_init.py` | One-time setup: creates the Neon schema (runs `schema.sql`), then verifies read-write parity with a sentinel round-trip |
17
+ | `hook_log_extractor.py` | Stop hook — reads per-session JSONL transcripts and ingests new `hook_*` attachment records into the `hook_events` table; idempotent via a UNIQUE constraint on `(source_jsonl_path, source_line_number)` |
18
+ | `hook_log_stop_wrapper.py` | Thin wrapper that invokes `hook_log_extractor.py` from the Stop lifecycle event |
19
+ | `schema.sql` | DDL for the `hook_events` table, `blocked_commands` view, and supporting indexes |
20
+ | `requirements-hook-logs.txt` | Runtime dependencies (`psycopg`) for the extractor |
21
+ | `requirements-hook-logs-dev.txt` | Dev/test dependencies |
22
+ | `test_hook_log_extractor.py` | Tests for the extractor |
23
+ | `test_hook_log_init.py` | Tests for the schema-init script |
24
+ | `test_hook_log_stop_wrapper.py` | Tests for the Stop wrapper |
25
+
26
+ ## Schema overview (`schema.sql`)
27
+
28
+ The `hook_events` table captures one row per hook firing:
29
+
30
+ - `hook_event`, `hook_name`, `hook_category` — what fired
31
+ - `outcome` — `allowed`, `blocked`, or `ask`
32
+ - `tool_name`, `command_excerpt` — what tool was called
33
+ - `session_id`, `git_branch`, `cwd` — context
34
+ - `duration_ms`, `exit_code` — timing and result
35
+ - `source_jsonl_path`, `source_line_number` — idempotency key
36
+
37
+ The `blocked_commands` view filters to `outcome = 'blocked'`.
38
+
39
+ ## Conventions
40
+
41
+ - The extractor exits 0 even when Neon is unreachable (offline-graceful); it logs to `OFFLINE_WARNING_LOG` and does not block session end.
42
+ - Constants for the extractor (table name, offset state file, timeout) live in `hooks_constants/hook_log_extractor_constants.py`.
43
+ - Tests run with `python -m pytest diagnostic/test_hook_log_*.py`.
@@ -0,0 +1,16 @@
1
+ # hooks/diagnostic/migrations
2
+
3
+ SQL migration files for the `hook_events` Neon schema. Each file applies a schema change to the `hook_events` table or its indexes.
4
+
5
+ ## Files
6
+
7
+ | File | What it does |
8
+ |---|---|
9
+ | `2026-04-25-drop-themes-hook-events.sql` | Drops the `themes` hook-events table variant |
10
+ | `README.md` | Notes on the migration approach |
11
+
12
+ ## Conventions
13
+
14
+ - Run migrations manually against the Neon database using `psql` or the Neon console.
15
+ - The baseline schema lives in `diagnostic/schema.sql`.
16
+ - File names follow `YYYY-MM-DD-<description>.sql` for chronological ordering.
@@ -0,0 +1,19 @@
1
+ # hooks/diagnostic/queries
2
+
3
+ Parameterized SQL queries for inspecting the `hook_events` Neon table. Run these directly against the Neon database to analyze hook-firing patterns.
4
+
5
+ ## Files
6
+
7
+ | File | What it returns |
8
+ |---|---|
9
+ | `block_details_for_hook.sql` | Full details for all blocked events matching a given hook name |
10
+ | `blocks_by_category.sql` | Count of blocks grouped by hook category |
11
+ | `blocks_by_tool.sql` | Count of blocks grouped by tool name |
12
+ | `blocks_last_7_days.sql` | All blocked events from the last 7 days |
13
+ | `top_blockers_last_24_hours.sql` | Hook names with the most blocks in the last 24 hours |
14
+ | `top_blockers_overall.sql` | Hook names with the most blocks across all time |
15
+
16
+ ## Conventions
17
+
18
+ - Queries target the `hook_events` table and `blocked_commands` view defined in `diagnostic/schema.sql`.
19
+ - Run with `psql $DATABASE_URL -f <query>.sql` or paste into the Neon console.
@@ -0,0 +1,28 @@
1
+ # hooks/git-hooks
2
+
3
+ Native git hooks that run outside the Claude Code lifecycle — invoked directly by git at commit and push time. The installer copies these scripts into the user's shared git-hooks directory (`core.hooksPath`).
4
+
5
+ ## Key files
6
+
7
+ | File | Git hook | What it does |
8
+ |---|---|---|
9
+ | `pre_commit.py` | `pre-commit` | Runs the CODE_RULES gate (`precommit_code_rules_gate.py`) over staged changes; exits 1 when any staged file has a blocking violation |
10
+ | `pre_push.py` | `pre-push` | Runs the verified-commit gate check before a push reaches the remote |
11
+ | `post_commit.py` | `post-commit` | Runs after a commit lands; performs any post-commit bookkeeping |
12
+ | `gate_utils.py` | — | Shared helpers: resolves the gate script path, checks that the path is a safe regular file |
13
+ | `test_config.py` | — | Test configuration helpers |
14
+ | `test_gate_utils.py` | — | Tests for `gate_utils.py` |
15
+ | `test_pre_commit.py` | — | Tests for `pre_commit.py` |
16
+ | `test_pre_push.py` | — | Tests for `pre_push.py` |
17
+
18
+ ## Subdirectory
19
+
20
+ | Directory | Role |
21
+ |---|---|
22
+ | `git_hooks_constants/` | Shared constants imported by the git-hook scripts |
23
+
24
+ ## Conventions
25
+
26
+ - The installer strips the `_` and `.py` suffix when copying into the live git-hooks path (e.g. `pre_commit.py` becomes `pre-commit`).
27
+ - Constants (exit codes, argument names, error messages) live in `git_hooks_constants/` and are imported at the top of each script.
28
+ - Run tests with `python -m pytest git-hooks/test_<name>.py`.
@@ -0,0 +1,21 @@
1
+ # hooks/git-hooks/git_hooks_constants
2
+
3
+ Shared constants imported by the git-hook scripts in `git-hooks/`. Centralizes exit codes, argument names, and error messages so every tunable lives in one place.
4
+
5
+ ## Files
6
+
7
+ | File | Contents |
8
+ |---|---|
9
+ | `__init__.py` | Exports all constants; marks this as a package so `from git_hooks_constants import ...` resolves |
10
+
11
+ ## Key constants (defined in `__init__.py`)
12
+
13
+ - `GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE` — exit code when the gate script cannot be found or launched
14
+ - `GATE_SCRIPT_NOT_FOUND_MESSAGE` — error message when the gate script path does not exist
15
+ - `INVOKE_GATE_FAILURE_MESSAGE` — error message when the gate subprocess fails to start
16
+ - `STAGED_SCOPE_ARGUMENT` — CLI argument passed to the gate script to scope it to staged changes
17
+
18
+ ## Conventions
19
+
20
+ - Import with `from git_hooks_constants import <CONSTANT>` from within the `git-hooks/` directory.
21
+ - Add new constants here rather than inline in the hook scripts.
@@ -0,0 +1,60 @@
1
+ # hooks/hooks_constants
2
+
3
+ Shared constant modules imported by hooks throughout the `hooks/` tree. Each file holds the tunables for one hook or one cross-cutting concern, keeping magic values out of the hook scripts themselves.
4
+
5
+ ## Files
6
+
7
+ | File | What it holds |
8
+ |---|---|
9
+ | `__init__.py` | Package marker (`# pragma: no-tdd-gate`) |
10
+ | `any_type_config.py` | Config for the `Any`-type escape-hatch check |
11
+ | `banned_identifiers_constants.py` | The set of banned short identifiers and banned function-name prefixes |
12
+ | `blocking_check_limits.py` | Max issue counts and preview lengths for blocking hooks |
13
+ | `bot_mention_comment_blocker_constants.py` | Patterns for detecting bot @-mentions in PR comments |
14
+ | `code_rules_enforcer_constants.py` | File-extension sets, test-path patterns, advisory line thresholds, boolean-name prefixes |
15
+ | `code_rules_path_utils_constants.py` | Path-matching helpers used by the code-rules check modules |
16
+ | `convergence_branch_constants.py` | Branch and worktree naming patterns for the convergence gate |
17
+ | `dead_argparse_argument_constants.py` | Patterns for detecting unused argparse arguments |
18
+ | `dead_config_field_constants.py` | Patterns for detecting unused dataclass config fields |
19
+ | `dead_dataclass_field_constants.py` | Patterns for detecting unused dataclass fields |
20
+ | `dead_module_constant_constants.py` | Patterns for detecting unexported `UPPER_SNAKE` constants in `*_constants.py` modules |
21
+ | `destructive_command_segment_constants.py` | The list of destructive shell command patterns the blocker matches |
22
+ | `doc_gist_auto_publish_constants.py` | Sentinel marker and URL patterns for the doc-gist auto-publish hook |
23
+ | `duplicate_function_body_constants.py` | Hashing and comparison config for the duplicate-body check |
24
+ | `dynamic_stderr_handler.py` | `DynamicStderrHandler` — a logging handler that resolves `sys.stderr` at emit time (for testability) |
25
+ | `gh_pr_author_swap_constants.py` | Constants for the PR-author swap enforcement hooks |
26
+ | `hardcoded_user_path_constants.py` | Patterns for detecting hardcoded home-directory paths |
27
+ | `hook_log_extractor_constants.py` | Neon table name, offset state file path, timeouts, and outcome-type mapping for the hook-log extractor |
28
+ | `hook_prose_detector_consistency_constants.py` | Trigger patterns and corrective messages for the hook-prose consistency checker |
29
+ | `html_companion_constants.py` | Blocked URL schemes and other config for the `.md`-to-`.html` companion hook |
30
+ | `inline_tuple_string_magic_constants.py` | Patterns for detecting magic strings in inline tuple literals |
31
+ | `md_to_html_blocker_constants.py` | Path exemptions and trigger patterns for the markdown-to-html blocker |
32
+ | `messages.py` | Short user-facing notice strings shown when a Stop hook redirects agent behavior |
33
+ | `open_questions_in_plans_blocker_constants.py` | Patterns for detecting unresolved open questions in plan documents |
34
+ | `orphan_css_class_constants.py` | Scan radius and selector patterns for the orphan-CSS-class check |
35
+ | `path_rewriter_constants.py` | Path rewriting patterns for the Everything-search path rewriter |
36
+ | `plain_language_blocker_constants.py` | The list of heavy words and their everyday replacements |
37
+ | `pr_converge_bugteam_enforcer_constants.py` | State keys and timing config for the bugteam-parallel enforcer |
38
+ | `pr_converge_bugteam_enforcer_state.py` | State-file helpers for the bugteam enforcer |
39
+ | `pr_description_enforcer_constants.py` | PR-description shape rules and command patterns |
40
+ | `pre_tool_use_stdin.py` | `read_hook_input_dictionary_from_stdin()` — shared stdin parser for PreToolUse hooks |
41
+ | `precommit_code_rules_gate_constants.py` | Scope argument and exit-code constants for the precommit gate |
42
+ | `project_paths_reader.py` | Loads `~/.claude/project-paths.json` — the per-user project-path registry |
43
+ | `session_env_cleanup_constants.py` | Stale-age threshold and directory names for the session-env cleanup hook |
44
+ | `session_handoff_blocker_constants.py` | Trigger phrases for the session-handoff blocker |
45
+ | `setup_project_paths_constants.py` | Encoding policy, BOM marker, and registry meta-key used across multiple hooks |
46
+ | `state_description_blocker_constants.py` | The set of historical/comparative phrases the state-description blocker rejects |
47
+ | `stuttering_check_config.py` | Config for the stuttering (repeated-phrase) check |
48
+ | `stuttering_import_binding_constants.py` | Import-binding patterns for the stuttering check |
49
+ | `subprocess_budget_completeness_constants.py` | Required argument names for the subprocess-budget completeness check |
50
+ | `sys_path_insert_constants.py` | Patterns for detecting unguarded `sys.path.insert` calls |
51
+ | `unused_module_import_constants.py` | Patterns for detecting unused module-level imports |
52
+ | `windows_rmtree_blocker_constants.py` | The unsafe `shutil.rmtree` pattern and the safe replacement pattern |
53
+ | `workflow_substitution_slot_blocker_constants.py` | Per-iteration token patterns for the workflow-slot blocker |
54
+
55
+ ## Conventions
56
+
57
+ - Every file in this package is a pure constants module — no side effects, no I/O.
58
+ - Hooks import from this package with `from hooks_constants.<module> import <CONSTANT>`.
59
+ - Tests for these modules live beside them as `test_<module>.py`. Run with `python -m pytest hooks_constants/test_<name>.py`.
60
+ - `dynamic_stderr_handler.py` and `pre_tool_use_stdin.py` are utility modules (not pure constants) but live here because they are shared across many hooks.
@@ -17,6 +17,8 @@ MAX_BOUNDARY_TYPE_ISSUES: int = 5
17
17
  ALL_BANNED_PREFIX_NAMES: tuple[str, ...] = ("handle_", "process_", "manage_", "do_")
18
18
  MAX_DOCSTRING_FORMAT_ISSUES: int = 5
19
19
  MAX_DOCSTRING_ARGS_SIGNATURE_ISSUES: int = 5
20
+ MAX_CLASS_DOCSTRING_PUBLIC_METHOD_ISSUES: int = 5
21
+ MINIMUM_PUBLIC_METHODS_FOR_CLASS_DOCSTRING_BREADTH: int = 2
20
22
  MAX_IGNORED_MUST_CHECK_RETURN_ISSUES: int = 5
21
23
  MAX_TYPE_ESCAPE_HATCH_ISSUES: int = 5
22
24
  MAX_THIN_WRAPPER_ISSUES: int = 1
@@ -25,6 +27,22 @@ MAX_LOGGING_FSTRING_ISSUES: int = 3
25
27
  MAX_WINDOWS_API_NONE_ISSUES: int = 3
26
28
  MAX_E2E_TEST_NAMING_ISSUES: int = 3
27
29
  DOCSTRING_TRIVIAL_FUNCTION_BODY_LINE_LIMIT: int = 3
30
+ MAX_DOCSTRING_FALLBACK_BRANCH_ISSUES: int = 3
31
+ DOCSTRING_FALLBACK_BRANCH_MINIMUM_ROUTE_COUNT: int = 2
32
+
33
+ ALL_DOCSTRING_EXCLUSIVE_SCOPE_PHRASES: tuple[str, ...] = (
34
+ "only when",
35
+ "only if",
36
+ "falls back to",
37
+ "falling back to",
38
+ "fall back to",
39
+ )
40
+
41
+ ALL_DOCSTRING_MULTIPLE_CONDITION_JOINING_PHRASES: tuple[str, ...] = (
42
+ " or ",
43
+ "either",
44
+ "both",
45
+ )
28
46
 
29
47
  ALL_BARE_EXCEPT_BANNED_HANDLER_NAMES: frozenset[str] = frozenset({"Exception", "BaseException"})
30
48
  ALL_BOUNDARY_TYPE_EXEMPT_FILENAMES: frozenset[str] = frozenset({"protocols.py", "types.py"})
@@ -0,0 +1,18 @@
1
+ # hooks/lifecycle
2
+
3
+ Hooks that run at session or config-change boundaries rather than on individual tool calls.
4
+
5
+ ## Key files
6
+
7
+ | File | Event | What it does |
8
+ |---|---|---|
9
+ | `config_change_guard.py` | PostToolUse (Write/Edit on `settings.json`) | Counts hooks in the edited `settings.json` and logs any change to `~/.claude/cache/config-change-audit.log`; alerts when the hook count drops below the last known value |
10
+ | `pr_converge_bugteam_skill_tracker.py` | PostToolUse | Tracks which bugteam skill runs have completed within a pr-converge loop, so the enforcer can verify parallel execution |
11
+ | `session_end_cleanup.py` | SessionEnd | Purges stale cache entries from `~/.claude/cache/` (entries older than the configured threshold) and old backup files |
12
+ | `test_config_change_guard.py` | — | Tests for `config_change_guard.py` |
13
+ | `test_pr_converge_bugteam_skill_tracker.py` | — | Tests for `pr_converge_bugteam_skill_tracker.py` |
14
+
15
+ ## Conventions
16
+
17
+ - Constants for these hooks (stale-age threshold, cache directory, known-hook-count file) live in `hooks_constants/session_env_cleanup_constants.py` and `hooks_constants/pr_converge_bugteam_enforcer_constants.py`.
18
+ - Tests run with `python -m pytest lifecycle/test_<name>.py`.