claude-dev-env 1.68.0 → 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 (120) 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/prompts/CLAUDE.md +36 -0
  10. package/bin/CLAUDE.md +28 -0
  11. package/commands/CLAUDE.md +25 -0
  12. package/docs/CLAUDE.md +28 -0
  13. package/docs/references/CLAUDE.md +13 -0
  14. package/hooks/CLAUDE.md +31 -0
  15. package/hooks/advisory/CLAUDE.md +16 -0
  16. package/hooks/blocking/CLAUDE.md +107 -0
  17. package/hooks/blocking/code_rules_constants_config.py +7 -4
  18. package/hooks/blocking/code_rules_docstrings.py +97 -0
  19. package/hooks/blocking/code_rules_enforcer.py +4 -0
  20. package/hooks/blocking/config/CLAUDE.md +22 -0
  21. package/hooks/blocking/test_code_rules_enforcer_class_docstring_methods.py +262 -0
  22. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +36 -0
  23. package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +9 -0
  24. package/hooks/diagnostic/CLAUDE.md +43 -0
  25. package/hooks/diagnostic/migrations/CLAUDE.md +16 -0
  26. package/hooks/diagnostic/queries/CLAUDE.md +19 -0
  27. package/hooks/git-hooks/CLAUDE.md +28 -0
  28. package/hooks/git-hooks/git_hooks_constants/CLAUDE.md +21 -0
  29. package/hooks/hooks_constants/CLAUDE.md +60 -0
  30. package/hooks/hooks_constants/blocking_check_limits.py +2 -0
  31. package/hooks/lifecycle/CLAUDE.md +18 -0
  32. package/hooks/observability/CLAUDE.md +16 -0
  33. package/hooks/session/CLAUDE.md +21 -0
  34. package/hooks/validation/CLAUDE.md +19 -0
  35. package/hooks/validators/CLAUDE.md +49 -0
  36. package/hooks/workflow/CLAUDE.md +22 -0
  37. package/package.json +1 -1
  38. package/rules/CLAUDE.md +46 -0
  39. package/rules/docstring-prose-matches-implementation.md +1 -1
  40. package/scripts/CLAUDE.md +34 -0
  41. package/scripts/dev_env_scripts_constants/CLAUDE.md +14 -0
  42. package/scripts/sync_to_cursor/CLAUDE.md +23 -0
  43. package/scripts/tests/CLAUDE.md +18 -0
  44. package/skills/CLAUDE.md +66 -0
  45. package/skills/_shared/CLAUDE.md +11 -0
  46. package/skills/_shared/pr-loop/CLAUDE.md +27 -0
  47. package/skills/_shared/pr-loop/prompts/CLAUDE.md +9 -0
  48. package/skills/_shared/pr-loop/scripts/CLAUDE.md +23 -0
  49. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +19 -0
  50. package/skills/anthropic-plan/CLAUDE.md +34 -0
  51. package/skills/anthropic-plan/SKILL.md +1 -1
  52. package/skills/anthropic-plan/scripts/CLAUDE.md +11 -0
  53. package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/CLAUDE.md +16 -0
  54. package/skills/anthropic-plan/templates/CLAUDE.md +13 -0
  55. package/skills/anthropic-plan/workflow/CLAUDE.md +14 -0
  56. package/skills/auditing-claude-config/CLAUDE.md +20 -0
  57. package/skills/autoconverge/CLAUDE.md +30 -0
  58. package/skills/autoconverge/reference/CLAUDE.md +12 -0
  59. package/skills/autoconverge/workflow/CLAUDE.md +23 -0
  60. package/skills/autoconverge/workflow/autoconverge_report_constants/CLAUDE.md +16 -0
  61. package/skills/bdd-protocol/CLAUDE.md +26 -0
  62. package/skills/bdd-protocol/references/CLAUDE.md +10 -0
  63. package/skills/bg-agent/CLAUDE.md +17 -0
  64. package/skills/bugteam/CLAUDE.md +30 -0
  65. package/skills/bugteam/reference/CLAUDE.md +22 -0
  66. package/skills/bugteam/reference/obstacles/CLAUDE.md +24 -0
  67. package/skills/bugteam/scripts/CLAUDE.md +36 -0
  68. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +20 -0
  69. package/skills/caveman/CLAUDE.md +15 -0
  70. package/skills/code/CLAUDE.md +17 -0
  71. package/skills/copilot-review/CLAUDE.md +17 -0
  72. package/skills/deep-research/CLAUDE.md +17 -0
  73. package/skills/doc-gist/CLAUDE.md +25 -0
  74. package/skills/doc-gist/references/CLAUDE.md +9 -0
  75. package/skills/doc-gist/references/examples/CLAUDE.md +25 -0
  76. package/skills/doc-gist/scripts/CLAUDE.md +27 -0
  77. package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +10 -0
  78. package/skills/everything-search/CLAUDE.md +17 -0
  79. package/skills/findbugs/CLAUDE.md +20 -0
  80. package/skills/fixbugs/CLAUDE.md +19 -0
  81. package/skills/fresh-branch/CLAUDE.md +15 -0
  82. package/skills/gh-paginate/CLAUDE.md +18 -0
  83. package/skills/gotcha/CLAUDE.md +33 -0
  84. package/skills/implement/CLAUDE.md +27 -0
  85. package/skills/implement/scripts/CLAUDE.md +22 -0
  86. package/skills/implement/scripts/implement_scripts_constants/CLAUDE.md +22 -0
  87. package/skills/logifix/CLAUDE.md +36 -0
  88. package/skills/logifix/scripts/CLAUDE.md +16 -0
  89. package/skills/monitor-open-prs/CLAUDE.md +34 -0
  90. package/skills/monitor-open-prs/scripts/CLAUDE.md +17 -0
  91. package/skills/pr-consistency-audit/CLAUDE.md +34 -0
  92. package/skills/pr-consistency-audit/reference/CLAUDE.md +16 -0
  93. package/skills/pr-converge/CLAUDE.md +29 -0
  94. package/skills/pr-converge/pr_converge_skill_constants/CLAUDE.md +26 -0
  95. package/skills/pr-converge/reference/CLAUDE.md +27 -0
  96. package/skills/pr-converge/reference/obstacles/CLAUDE.md +23 -0
  97. package/skills/pr-converge/scripts/CLAUDE.md +36 -0
  98. package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +17 -0
  99. package/skills/pr-converge/workflows/CLAUDE.md +16 -0
  100. package/skills/pr-review-responder/CLAUDE.md +35 -0
  101. package/skills/pre-compact/CLAUDE.md +24 -0
  102. package/skills/qbug/CLAUDE.md +40 -0
  103. package/skills/rebase/CLAUDE.md +32 -0
  104. package/skills/recall/CLAUDE.md +30 -0
  105. package/skills/refine/CLAUDE.md +44 -0
  106. package/skills/refine/templates/CLAUDE.md +17 -0
  107. package/skills/remember/CLAUDE.md +31 -0
  108. package/skills/research-mode/CLAUDE.md +35 -0
  109. package/skills/session-log/CLAUDE.md +31 -0
  110. package/skills/session-tidy/CLAUDE.md +36 -0
  111. package/skills/skill-builder/CLAUDE.md +45 -0
  112. package/skills/skill-builder/references/CLAUDE.md +19 -0
  113. package/skills/skill-builder/templates/CLAUDE.md +14 -0
  114. package/skills/skill-builder/workflows/CLAUDE.md +17 -0
  115. package/skills/structure-prompt/CLAUDE.md +42 -0
  116. package/skills/structure-prompt/reference/CLAUDE.md +28 -0
  117. package/skills/task-build/CLAUDE.md +28 -0
  118. package/skills/update/CLAUDE.md +38 -0
  119. package/skills/verified-build/CLAUDE.md +33 -0
  120. package/system-prompts/CLAUDE.md +17 -0
@@ -26,9 +26,11 @@ from hooks_constants.blocking_check_limits import ( # noqa: E402
26
26
  ALL_DOCSTRING_MULTIPLE_CONDITION_JOINING_PHRASES,
27
27
  DOCSTRING_FALLBACK_BRANCH_MINIMUM_ROUTE_COUNT,
28
28
  DOCSTRING_TRIVIAL_FUNCTION_BODY_LINE_LIMIT,
29
+ MAX_CLASS_DOCSTRING_PUBLIC_METHOD_ISSUES,
29
30
  MAX_DOCSTRING_ARGS_SIGNATURE_ISSUES,
30
31
  MAX_DOCSTRING_FALLBACK_BRANCH_ISSUES,
31
32
  MAX_DOCSTRING_FORMAT_ISSUES,
33
+ MINIMUM_PUBLIC_METHODS_FOR_CLASS_DOCSTRING_BREADTH,
32
34
  )
33
35
  from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
34
36
  ALL_DOCSTRING_ARGS_SECTION_HEADERS,
@@ -462,3 +464,98 @@ def check_docstring_fallback_branch_coverage(content: str, file_path: str) -> li
462
464
  if len(issues) >= MAX_DOCSTRING_FALLBACK_BRANCH_ISSUES:
463
465
  break
464
466
  return issues[:MAX_DOCSTRING_FALLBACK_BRANCH_ISSUES]
467
+
468
+
469
+ def _class_docstring_summary_is_single_line(docstring_text: str) -> bool:
470
+ stripped_text = docstring_text.strip()
471
+ if not stripped_text:
472
+ return False
473
+ summary_line, separator, _remainder = stripped_text.partition("\n")
474
+ if separator and stripped_text[len(summary_line):].strip():
475
+ return False
476
+ return bool(summary_line.strip())
477
+
478
+
479
+ def _public_method_names(class_node: ast.ClassDef) -> list[str]:
480
+ deduplicated_names: dict[str, None] = {}
481
+ for each_statement in class_node.body:
482
+ if not isinstance(each_statement, (ast.FunctionDef, ast.AsyncFunctionDef)):
483
+ continue
484
+ if _function_is_private_or_dunder(each_statement.name):
485
+ continue
486
+ deduplicated_names[each_statement.name] = None
487
+ return list(deduplicated_names)
488
+
489
+
490
+ def _name_tokens(method_name: str) -> list[str]:
491
+ return [each_token for each_token in method_name.split("_") if each_token]
492
+
493
+
494
+ def _docstring_mentions_method(docstring_text: str, method_name: str) -> bool:
495
+ lowered_docstring = docstring_text.lower()
496
+ if method_name.lower() in lowered_docstring:
497
+ return True
498
+ return all(
499
+ each_token.lower() in lowered_docstring for each_token in _name_tokens(method_name)
500
+ )
501
+
502
+
503
+ def _unmentioned_public_methods(class_node: ast.ClassDef, docstring_text: str) -> list[str]:
504
+ return [
505
+ each_name
506
+ for each_name in _public_method_names(class_node)
507
+ if not _docstring_mentions_method(docstring_text, each_name)
508
+ ]
509
+
510
+
511
+ def check_class_docstring_names_public_methods(
512
+ content: str, file_path: str
513
+ ) -> list[str]:
514
+ """Flag a one-line class docstring that omits two or more public methods.
515
+
516
+ A class whose docstring is a single summary line names one responsibility,
517
+ so a reader trusts that line to describe the whole class. When the class
518
+ later gains a second public entry point — the drift pattern where a
519
+ coffee-break reporter grows a regular-pace method — the terse summary keeps
520
+ describing only the original feature. Each public method whose name (or all
521
+ of its underscore-separated tokens) appears nowhere in the summary counts as
522
+ omitted; a class with two or more omitted public methods is reported so the
523
+ summary is widened to name the broader surface. Classes with a multi-line
524
+ docstring body are left to the audit lane, since their prose can carry the
525
+ enumeration without naming each method by name.
526
+
527
+ Args:
528
+ content: The source text to inspect.
529
+ file_path: The path the source will be written to, used for exemptions.
530
+
531
+ Returns:
532
+ One issue per class whose single-line docstring omits two or more of its
533
+ public methods, capped at the module limit.
534
+ """
535
+ if is_test_file(file_path) or is_hook_infrastructure(file_path):
536
+ return []
537
+ try:
538
+ parsed_tree = ast.parse(content)
539
+ except SyntaxError:
540
+ return []
541
+ issues: list[str] = []
542
+ for each_node in _walk_skipping_type_checking_blocks(parsed_tree):
543
+ if not isinstance(each_node, ast.ClassDef):
544
+ continue
545
+ class_docstring = ast.get_docstring(each_node) or ""
546
+ if not _class_docstring_summary_is_single_line(class_docstring):
547
+ continue
548
+ public_names = _public_method_names(each_node)
549
+ if len(public_names) < MINIMUM_PUBLIC_METHODS_FOR_CLASS_DOCSTRING_BREADTH:
550
+ continue
551
+ unmentioned_names = _unmentioned_public_methods(each_node, class_docstring)
552
+ if len(unmentioned_names) < MINIMUM_PUBLIC_METHODS_FOR_CLASS_DOCSTRING_BREADTH:
553
+ continue
554
+ issues.append(
555
+ f"Line {each_node.lineno}: {each_node.name} one-line docstring omits "
556
+ f"public method(s) {', '.join(unmentioned_names)} — widen the summary "
557
+ "so it names the class's full public surface"
558
+ )
559
+ if len(issues) >= MAX_CLASS_DOCSTRING_PUBLIC_METHOD_ISSUES:
560
+ break
561
+ return issues[:MAX_CLASS_DOCSTRING_PUBLIC_METHOD_ISSUES]
@@ -65,6 +65,7 @@ from code_rules_dead_module_constant import ( # noqa: E402
65
65
  check_dead_module_constants,
66
66
  )
67
67
  from code_rules_docstrings import ( # noqa: E402
68
+ check_class_docstring_names_public_methods,
68
69
  check_docstring_args_match_signature,
69
70
  check_docstring_fallback_branch_coverage,
70
71
  check_docstring_format,
@@ -250,6 +251,9 @@ def validate_content(
250
251
  all_issues.extend(check_docstring_format(effective_content, file_path))
251
252
  all_issues.extend(check_docstring_args_match_signature(effective_content, file_path))
252
253
  all_issues.extend(check_docstring_fallback_branch_coverage(effective_content, file_path))
254
+ all_issues.extend(
255
+ check_class_docstring_names_public_methods(effective_content, file_path)
256
+ )
253
257
  all_issues.extend(
254
258
  check_boolean_naming(
255
259
  effective_content,
@@ -0,0 +1,22 @@
1
+ # hooks/blocking/config
2
+
3
+ A Python package that holds shared constants for the verified-commit gate family. Three modules in `blocking/` import from here:
4
+
5
+ - `verification_verdict_store.py`
6
+ - `verified_commit_gate.py`
7
+ - `verifier_verdict_minter.py`
8
+
9
+ ## Key files
10
+
11
+ | File | Contents |
12
+ |---|---|
13
+ | `__init__.py` | Declares this as a regular package (not a namespace package) so it resolves first on `sys.path` |
14
+ | `verified_commit_constants.py` | All tunables for the gate: directory names, regex patterns for detecting verdict paths and obfuscation attempts, timeout values, git subcommand sets, bypass marker, and corrective messages |
15
+
16
+ ## Key constants in `verified_commit_constants.py`
17
+
18
+ - `VERIFICATION_BYPASS_MARKER` — the `# verify-skip` comment that exempts a single commit/push from the gate
19
+ - `MINTING_AGENT_TYPE` — `"code-verifier"`, the agent type whose SubagentStop hook mints verdicts
20
+ - `VERDICT_DIRECTORY_NAME` — `"verification"`, the directory under `~/.claude/` that holds verdict JSON files
21
+ - `DOCS_ONLY_EXTENSIONS` — extensions (`.md`, `.txt`, images) whose changes are mechanically exempt from the gate
22
+ - `CORRECTIVE_MESSAGE` / `VERDICT_DIRECTORY_GUARD_MESSAGE` — user-facing block messages
@@ -0,0 +1,262 @@
1
+ """Tests for check_class_docstring_names_public_methods — class prose breadth.
2
+
3
+ A class whose docstring is a single summary line names one responsibility. When
4
+ the class exposes a second public entry point the summary never names, the prose
5
+ under-describes the class — the same drift the os_update_workflow break reporter
6
+ hit when it grew a regular-pace method beside its coffee-break method.
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_class_docstring_names_public_methods(content: str, file_path: str) -> list[str]:
30
+ return code_rules_enforcer.check_class_docstring_names_public_methods(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/break_reporter.py"
38
+ TEST_FILE_PATH = "/project/src/test_break_reporter.py"
39
+ HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
40
+
41
+
42
+ def _narrow_class_with_widened_surface() -> str:
43
+ return (
44
+ "class ConsoleBreakReporter:\n"
45
+ ' """Run a coffee break with operator visibility: announce, then count down."""\n'
46
+ "\n"
47
+ " async def pause_then_resume(self, seconds: float) -> None:\n"
48
+ " await self._sleep(seconds)\n"
49
+ "\n"
50
+ " async def stretch_then_resume(self, seconds: float) -> None:\n"
51
+ " await self._sleep(seconds)\n"
52
+ )
53
+
54
+
55
+ def test_should_flag_single_line_docstring_omitting_two_public_methods() -> None:
56
+ issues = check_class_docstring_names_public_methods(
57
+ _narrow_class_with_widened_surface(), PRODUCTION_FILE_PATH
58
+ )
59
+ assert any("pause_then_resume" in each for each in issues), (
60
+ f"Expected omitted-method flag, got: {issues!r}"
61
+ )
62
+ assert any("stretch_then_resume" in each for each in issues)
63
+ assert len(issues) == 1
64
+
65
+
66
+ def test_should_not_flag_when_summary_names_every_public_method() -> None:
67
+ source = (
68
+ "class ConsoleBreakReporter:\n"
69
+ ' """Announce a pause then resume, or stretch then resume, with a countdown."""\n'
70
+ "\n"
71
+ " async def pause_then_resume(self, seconds: float) -> None:\n"
72
+ " await self._sleep(seconds)\n"
73
+ "\n"
74
+ " async def stretch_then_resume(self, seconds: float) -> None:\n"
75
+ " await self._sleep(seconds)\n"
76
+ )
77
+ issues = check_class_docstring_names_public_methods(source, PRODUCTION_FILE_PATH)
78
+ assert issues == [], f"Summary naming every method must not flag, got: {issues!r}"
79
+
80
+
81
+ def test_should_not_flag_when_only_one_public_method_is_omitted() -> None:
82
+ source = (
83
+ "class ConsoleBreakReporter:\n"
84
+ ' """Pause then resume the submission run with an operator countdown."""\n'
85
+ "\n"
86
+ " async def pause_then_resume(self, seconds: float) -> None:\n"
87
+ " await self._sleep(seconds)\n"
88
+ "\n"
89
+ " async def stretch_then_resume(self, seconds: float) -> None:\n"
90
+ " await self._sleep(seconds)\n"
91
+ )
92
+ issues = check_class_docstring_names_public_methods(source, PRODUCTION_FILE_PATH)
93
+ assert issues == [], f"A single omitted method must not flag, got: {issues!r}"
94
+
95
+
96
+ def test_should_not_flag_multi_line_docstring_body() -> None:
97
+ source = (
98
+ "class ConsoleBreakReporter:\n"
99
+ ' """Run a coffee break with operator visibility.\n'
100
+ "\n"
101
+ " Also paces the regular between-theme waits through the same seam.\n"
102
+ ' """\n'
103
+ "\n"
104
+ " async def pause_then_resume(self, seconds: float) -> None:\n"
105
+ " await self._sleep(seconds)\n"
106
+ "\n"
107
+ " async def stretch_then_resume(self, seconds: float) -> None:\n"
108
+ " await self._sleep(seconds)\n"
109
+ )
110
+ issues = check_class_docstring_names_public_methods(source, PRODUCTION_FILE_PATH)
111
+ assert issues == [], f"Multi-line docstrings go to the audit lane, got: {issues!r}"
112
+
113
+
114
+ def test_should_not_flag_class_with_single_public_method() -> None:
115
+ source = (
116
+ "class ConsoleBreakReporter:\n"
117
+ ' """Run a coffee break with operator visibility."""\n'
118
+ "\n"
119
+ " async def pause_then_resume(self, seconds: float) -> None:\n"
120
+ " await self._sleep(seconds)\n"
121
+ )
122
+ issues = check_class_docstring_names_public_methods(source, PRODUCTION_FILE_PATH)
123
+ assert issues == [], f"A one-method class must not flag, got: {issues!r}"
124
+
125
+
126
+ def test_should_skip_private_methods_when_counting_surface() -> None:
127
+ source = (
128
+ "class ConsoleBreakReporter:\n"
129
+ ' """Run a coffee break with operator visibility."""\n'
130
+ "\n"
131
+ " async def pause_then_resume(self, seconds: float) -> None:\n"
132
+ " await self._sleep(seconds)\n"
133
+ "\n"
134
+ " async def _sleep(self, seconds: float) -> None:\n"
135
+ " await self._clock.sleep(seconds)\n"
136
+ "\n"
137
+ " def __init__(self) -> None:\n"
138
+ " self._clock = None\n"
139
+ )
140
+ issues = check_class_docstring_names_public_methods(source, PRODUCTION_FILE_PATH)
141
+ assert issues == [], f"Private and dunder methods are not public surface, got: {issues!r}"
142
+
143
+
144
+ def test_should_skip_class_without_docstring() -> None:
145
+ source = (
146
+ "class ConsoleBreakReporter:\n"
147
+ " async def pause_then_resume(self, seconds: float) -> None:\n"
148
+ " await self._sleep(seconds)\n"
149
+ "\n"
150
+ " async def stretch_then_resume(self, seconds: float) -> None:\n"
151
+ " await self._sleep(seconds)\n"
152
+ )
153
+ issues = check_class_docstring_names_public_methods(source, PRODUCTION_FILE_PATH)
154
+ assert issues == [], f"No-docstring classes are out of scope, got: {issues!r}"
155
+
156
+
157
+ def test_should_skip_test_file() -> None:
158
+ issues = check_class_docstring_names_public_methods(
159
+ _narrow_class_with_widened_surface(), TEST_FILE_PATH
160
+ )
161
+ assert issues == [], f"Test files exempt, got: {issues!r}"
162
+
163
+
164
+ def test_should_skip_hook_infrastructure() -> None:
165
+ issues = check_class_docstring_names_public_methods(
166
+ _narrow_class_with_widened_surface(), HOOK_INFRASTRUCTURE_PATH
167
+ )
168
+ assert issues == [], f"Hook infrastructure exempt, got: {issues!r}"
169
+
170
+
171
+ def test_should_handle_syntax_error_gracefully() -> None:
172
+ issues = check_class_docstring_names_public_methods("class Broken(\n", PRODUCTION_FILE_PATH)
173
+ assert issues == [], f"Syntax error must yield no issues, got: {issues!r}"
174
+
175
+
176
+ def _real_break_reporter_drift() -> str:
177
+ return (
178
+ "class ConsoleBreakReporter:\n"
179
+ ' """Run a coffee-break with operator visibility: announce, then count down."""\n'
180
+ "\n"
181
+ " async def announce_and_pause(self, nominal_break_seconds: float) -> None:\n"
182
+ " await self._announce(nominal_break_seconds)\n"
183
+ "\n"
184
+ " async def announce_and_pause_exact(self, break_seconds: float) -> None:\n"
185
+ " await self._announce(break_seconds)\n"
186
+ )
187
+
188
+
189
+ def test_should_flag_real_break_reporter_widened_surface() -> None:
190
+ issues = check_class_docstring_names_public_methods(
191
+ _real_break_reporter_drift(), PRODUCTION_FILE_PATH
192
+ )
193
+ assert any("announce_and_pause_exact" in each for each in issues), (
194
+ f"Expected the regular-pace method to flag, got: {issues!r}"
195
+ )
196
+ assert any("announce_and_pause" in each for each in issues)
197
+ assert len(issues) == 1
198
+
199
+
200
+ def _overload_class_omitting_the_only_method() -> str:
201
+ return (
202
+ "from typing import overload\n"
203
+ "\n"
204
+ "class PayloadTransformer:\n"
205
+ ' """Hold a fixed payload value for later use."""\n'
206
+ "\n"
207
+ " @overload\n"
208
+ " def transform(self, payload: str) -> dict[str, str]: ...\n"
209
+ "\n"
210
+ " @overload\n"
211
+ " def transform(self, payload: bytes) -> dict[str, str]: ...\n"
212
+ "\n"
213
+ " def transform(self, payload: str | bytes) -> dict[str, str]:\n"
214
+ " return self._normalize(payload)\n"
215
+ )
216
+
217
+
218
+ def test_should_not_flag_single_overloaded_method_below_breadth_threshold() -> None:
219
+ issues = check_class_docstring_names_public_methods(
220
+ _overload_class_omitting_the_only_method(), PRODUCTION_FILE_PATH
221
+ )
222
+ assert issues == [], f"One overloaded public method must not flag, got: {issues!r}"
223
+
224
+
225
+ def test_should_not_repeat_an_overloaded_name_in_the_issue_message() -> None:
226
+ source = (
227
+ "from typing import overload\n"
228
+ "\n"
229
+ "class PayloadTransformer:\n"
230
+ ' """Hold a fixed payload value for later use."""\n'
231
+ "\n"
232
+ " @overload\n"
233
+ " def transform(self, payload: str) -> dict[str, str]: ...\n"
234
+ "\n"
235
+ " @overload\n"
236
+ " def transform(self, payload: bytes) -> dict[str, str]: ...\n"
237
+ "\n"
238
+ " def transform(self, payload: str | bytes) -> dict[str, str]:\n"
239
+ " return self._normalize(payload)\n"
240
+ "\n"
241
+ " def reset(self) -> None:\n"
242
+ " self._payload = None\n"
243
+ )
244
+ issues = check_class_docstring_names_public_methods(source, PRODUCTION_FILE_PATH)
245
+ assert len(issues) == 1, f"Expected a single drift issue, got: {issues!r}"
246
+ only_issue = issues[0]
247
+ assert only_issue.count("transform") == 1, (
248
+ f"Overloaded name must appear once in the message, got: {only_issue!r}"
249
+ )
250
+ assert "reset" in only_issue
251
+
252
+
253
+ def test_validate_content_surfaces_class_docstring_breadth_drift() -> None:
254
+ issues = validate_content(
255
+ _narrow_class_with_widened_surface(), PRODUCTION_FILE_PATH, old_content=""
256
+ )
257
+ matching_issues = [
258
+ each for each in issues if "pause_then_resume" in each and "public method" in each
259
+ ]
260
+ assert matching_issues, (
261
+ f"Expected validate_content to surface the class-breadth drift, got: {issues!r}"
262
+ )
@@ -56,6 +56,12 @@ def _check(source: str, file_path: str) -> list[str]:
56
56
  return code_rules_enforcer.check_dead_module_constants(source, file_path)
57
57
 
58
58
 
59
+ def _check_edit(fragment: str, full_file_content: str, file_path: str) -> list[str]:
60
+ return code_rules_enforcer.check_dead_module_constants(
61
+ fragment, file_path, full_file_content
62
+ )
63
+
64
+
59
65
  def _build_constants_package(
60
66
  workflow_directory: Path,
61
67
  constants_body: str,
@@ -94,6 +100,36 @@ def test_flags_constant_imported_by_no_module_in_the_tree(neutral_root: Path) ->
94
100
  ), f"Imported constants must not be flagged, got: {issues}"
95
101
 
96
102
 
103
+ def test_flags_a_dead_constant_added_by_an_edit_to_an_existing_module(
104
+ neutral_root: Path,
105
+ ) -> None:
106
+ consumer_body = (
107
+ "from report_constants.render_report_constants import (\n"
108
+ " MEDIUM_CODE,\n"
109
+ " MEDIUM_TERMINAL,\n"
110
+ ")\n"
111
+ "\n"
112
+ "def panel_class(medium: str) -> str:\n"
113
+ " if medium == MEDIUM_TERMINAL:\n"
114
+ " return 'terminal'\n"
115
+ " return 'code-panel' if medium == MEDIUM_CODE else 'text-panel'\n"
116
+ )
117
+ prior_body = 'MEDIUM_TERMINAL = "terminal"\nMEDIUM_CODE = "code"\n'
118
+ constants_path = _build_constants_package(
119
+ neutral_root / "workflow", prior_body, consumer_body
120
+ )
121
+ edit_fragment = 'MEDIUM_CODE = "code"\nMEDIUM_TEXT = "text"\n'
122
+ post_edit_body = prior_body + 'MEDIUM_TEXT = "text"\n'
123
+ issues = _check_edit(edit_fragment, post_edit_body, str(constants_path))
124
+ assert any("MEDIUM_TEXT" in each_issue for each_issue in issues), (
125
+ f"An Edit that inserts a dead constant must be flagged, got: {issues}"
126
+ )
127
+ assert not any(
128
+ "MEDIUM_TERMINAL" in each_issue or "MEDIUM_CODE" in each_issue
129
+ for each_issue in issues
130
+ ), f"Imported constants must not be flagged on an edit, got: {issues}"
131
+
132
+
97
133
  def test_does_not_flag_constant_imported_one_directory_up(neutral_root: Path) -> None:
98
134
  consumer_uses_text = (
99
135
  "from report_constants.render_report_constants import (\n"
@@ -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.