claude-dev-env 1.83.0 → 1.85.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 (186) hide show
  1. package/_shared/pr-loop/audit-contract.md +24 -12
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +16 -5
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +8 -0
  6. package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
  7. package/_shared/pr-loop/scripts/terminology_sweep.py +85 -39
  8. package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +212 -0
  9. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +18 -0
  10. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +18 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +37 -0
  12. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +14 -0
  13. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +94 -4
  14. package/agents/clean-coder.md +3 -0
  15. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  16. package/bin/install.mjs +16 -8
  17. package/bin/install.test.mjs +17 -6
  18. package/commands/CLAUDE.md +0 -1
  19. package/docs/CODE_RULES.md +1 -1
  20. package/hooks/blocking/CLAUDE.md +0 -4
  21. package/hooks/blocking/claude_md_orphan_file_blocker.py +1 -1
  22. package/hooks/blocking/code_rules_constants_config.py +5 -0
  23. package/hooks/blocking/code_rules_docstrings.py +133 -19
  24. package/hooks/blocking/code_rules_enforcer.py +4 -0
  25. package/hooks/blocking/code_rules_imports_logging.py +10 -6
  26. package/hooks/blocking/code_rules_magic_values.py +5 -0
  27. package/hooks/blocking/code_rules_naming_collection.py +15 -8
  28. package/hooks/blocking/code_rules_paired_test.py +3 -2
  29. package/hooks/blocking/code_rules_shared.py +34 -0
  30. package/hooks/blocking/code_rules_string_magic.py +1 -1
  31. package/hooks/blocking/code_rules_unused_imports.py +2 -2
  32. package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
  33. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +29 -0
  34. package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +197 -0
  35. package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +27 -0
  36. package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +24 -0
  37. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +33 -0
  38. package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -0
  39. package/hooks/blocking/test_code_rules_magic_values.py +54 -0
  40. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +0 -6
  41. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +184 -0
  42. package/hooks/blocking/test_shared_stdin_adoption.py +5 -30
  43. package/hooks/hooks_constants/CLAUDE.md +0 -3
  44. package/hooks/hooks_constants/blocking_check_limits.py +4 -0
  45. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  46. package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +0 -1
  47. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -5
  48. package/hooks/validation/post_tool_use_dispatcher.py +1 -1
  49. package/hooks/validation/test_post_tool_use_dispatcher.py +51 -35
  50. package/hooks/workflow/CLAUDE.md +2 -8
  51. package/package.json +1 -1
  52. package/rules/CLAUDE.md +1 -1
  53. package/rules/docstring-prose-matches-implementation.md +1 -1
  54. package/rules/env-var-table-code-drift.md +1 -1
  55. package/rules/package-inventory-stale-entry.md +1 -1
  56. package/rules/paired-test-coverage.md +1 -1
  57. package/rules/plain-illustrative-docstrings.md +35 -1
  58. package/skills/CLAUDE.md +6 -1
  59. package/skills/_shared/pr-loop/CLAUDE.md +1 -0
  60. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  61. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
  62. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/handoff_constants.py +45 -0
  63. package/skills/_shared/pr-loop/scripts/test_write_handoff.py +201 -0
  64. package/skills/_shared/pr-loop/scripts/write_handoff.py +309 -0
  65. package/skills/autoconverge/SKILL.md +82 -58
  66. package/skills/autoconverge/reference/closing-report.md +6 -6
  67. package/skills/autoconverge/reference/convergence.md +17 -15
  68. package/skills/autoconverge/reference/gotchas.md +6 -3
  69. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +2 -9
  70. package/skills/autoconverge/workflow/converge.contract.test.mjs +87 -13
  71. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +1 -1
  72. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
  73. package/skills/autoconverge/workflow/converge.mjs +71 -74
  74. package/skills/autoconverge/workflow/render_report.py +7 -11
  75. package/skills/bugteam/CLAUDE.md +1 -1
  76. package/skills/bugteam/PROMPTS.md +7 -6
  77. package/skills/bugteam/SKILL.md +25 -72
  78. package/skills/bugteam/reference/CLAUDE.md +1 -3
  79. package/skills/bugteam/reference/README.md +1 -1
  80. package/skills/bugteam/reference/audit-and-teammates.md +1 -1
  81. package/skills/bugteam/reference/obstacles/fix-publish-summary.md +1 -1
  82. package/skills/bugteam/reference/team-setup.md +8 -7
  83. package/skills/bugteam/scripts/CLAUDE.md +0 -6
  84. package/skills/bugteam/scripts/README.md +0 -4
  85. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
  86. package/skills/code/SKILL.md +2 -0
  87. package/skills/copilot-review/CLAUDE.md +1 -1
  88. package/skills/copilot-review/SKILL.md +25 -23
  89. package/skills/findbugs/CLAUDE.md +2 -2
  90. package/skills/findbugs/SKILL.md +22 -83
  91. package/skills/fixbugs/SKILL.md +2 -4
  92. package/skills/log-audit/CLAUDE.md +20 -0
  93. package/skills/log-audit/SKILL.md +68 -0
  94. package/skills/log-audit/reference/CLAUDE.md +9 -0
  95. package/skills/log-audit/reference/charter.md +52 -0
  96. package/skills/log-audit/scripts/CLAUDE.md +27 -0
  97. package/skills/log-audit/scripts/cluster_recurrences.py +261 -0
  98. package/skills/log-audit/scripts/collect_log_window.py +199 -0
  99. package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +12 -0
  100. package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +23 -0
  101. package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +24 -0
  102. package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +49 -0
  103. package/skills/log-audit/scripts/mine_copilot_findings.py +302 -0
  104. package/skills/log-audit/scripts/test_cluster_recurrences.py +160 -0
  105. package/skills/log-audit/scripts/test_collect_log_window.py +111 -0
  106. package/skills/log-audit/scripts/test_mine_copilot_findings.py +126 -0
  107. package/skills/monitor-open-prs/SKILL.md +2 -2
  108. package/skills/post-audit-findings/SKILL.md +84 -0
  109. package/skills/pr-converge/CLAUDE.md +2 -0
  110. package/skills/pr-converge/SKILL.md +99 -60
  111. package/skills/pr-converge/reference/CLAUDE.md +1 -1
  112. package/skills/pr-converge/reference/convergence-gates.md +16 -19
  113. package/skills/pr-converge/reference/examples.md +5 -5
  114. package/skills/pr-converge/reference/fix-protocol.md +16 -43
  115. package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +1 -1
  116. package/skills/pr-converge/reference/per-tick.md +24 -45
  117. package/skills/pr-converge/reference/state-schema.md +27 -0
  118. package/skills/pr-converge/scripts/README.md +3 -5
  119. package/skills/pr-fix-protocol/SKILL.md +70 -0
  120. package/skills/pr-loop-lifecycle/SKILL.md +73 -0
  121. package/skills/{bugteam → pr-loop-lifecycle}/reference/teardown-publish-permissions.md +22 -24
  122. package/skills/pr-scope-resolve/SKILL.md +48 -0
  123. package/skills/qbug/CLAUDE.md +4 -4
  124. package/skills/qbug/SKILL.md +46 -144
  125. package/skills/qbug/test_qbug_skill_audit_schema.py +2 -2
  126. package/skills/qbug/test_qbug_skill_post_fix_audit.py +1 -1
  127. package/skills/reviewer-gates/SKILL.md +96 -0
  128. package/skills/session-log/CLAUDE.md +7 -7
  129. package/skills/session-log/SKILL.md +27 -44
  130. package/skills/test_markdown_link_integrity.py +103 -0
  131. package/commands/doc-gist.md +0 -16
  132. package/hooks/blocking/_md_to_html_blocker_test_support.py +0 -65
  133. package/hooks/blocking/conftest.py +0 -30
  134. package/hooks/blocking/md_path_exemptions.py +0 -224
  135. package/hooks/blocking/md_to_html_blocker.py +0 -155
  136. package/hooks/blocking/test_md_to_html_blocker_exemptions.py +0 -434
  137. package/hooks/blocking/test_md_to_html_blocker_extensions.py +0 -157
  138. package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +0 -336
  139. package/hooks/hooks_constants/doc_gist_auto_publish_constants.py +0 -18
  140. package/hooks/hooks_constants/html_companion_constants.py +0 -20
  141. package/hooks/hooks_constants/md_to_html_blocker_constants.py +0 -76
  142. package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +0 -125
  143. package/hooks/workflow/doc_gist_auto_publish.py +0 -144
  144. package/hooks/workflow/md_to_html_companion.py +0 -358
  145. package/hooks/workflow/test_doc_gist_auto_publish.py +0 -117
  146. package/hooks/workflow/test_md_to_html_companion.py +0 -613
  147. package/skills/bugteam/reference/audit-contract.md +0 -163
  148. package/skills/bugteam/scripts/_bugteam_permissions_common.py +0 -455
  149. package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +0 -69
  150. package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -280
  151. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -266
  152. package/skills/bugteam/scripts/test__bugteam_permissions_common.py +0 -160
  153. package/skills/bugteam/scripts/test_agent_config_carveout.py +0 -356
  154. package/skills/bugteam/scripts/test_bugteam_permissions_common.py +0 -140
  155. package/skills/doc-gist/CLAUDE.md +0 -25
  156. package/skills/doc-gist/SKILL.md +0 -97
  157. package/skills/doc-gist/references/CLAUDE.md +0 -9
  158. package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +0 -453
  159. package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +0 -515
  160. package/skills/doc-gist/references/examples/03-code-review-pr.html +0 -638
  161. package/skills/doc-gist/references/examples/04-code-understanding.html +0 -491
  162. package/skills/doc-gist/references/examples/05-design-system.html +0 -629
  163. package/skills/doc-gist/references/examples/06-component-variants.html +0 -605
  164. package/skills/doc-gist/references/examples/07-prototype-animation.html +0 -455
  165. package/skills/doc-gist/references/examples/08-prototype-interaction.html +0 -396
  166. package/skills/doc-gist/references/examples/09-slide-deck.html +0 -592
  167. package/skills/doc-gist/references/examples/10-svg-illustrations.html +0 -492
  168. package/skills/doc-gist/references/examples/11-status-report.html +0 -528
  169. package/skills/doc-gist/references/examples/12-incident-report.html +0 -596
  170. package/skills/doc-gist/references/examples/13-flowchart-diagram.html +0 -395
  171. package/skills/doc-gist/references/examples/14-research-feature-explainer.html +0 -381
  172. package/skills/doc-gist/references/examples/15-research-concept-explainer.html +0 -368
  173. package/skills/doc-gist/references/examples/16-implementation-plan.html +0 -702
  174. package/skills/doc-gist/references/examples/17-pr-writeup.html +0 -595
  175. package/skills/doc-gist/references/examples/18-editor-triage-board.html +0 -573
  176. package/skills/doc-gist/references/examples/19-editor-feature-flags.html +0 -663
  177. package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +0 -722
  178. package/skills/doc-gist/references/examples/21-decision-signoff.html +0 -546
  179. package/skills/doc-gist/references/examples/CLAUDE.md +0 -25
  180. package/skills/doc-gist/references/examples/README.md +0 -5
  181. package/skills/doc-gist/scripts/CLAUDE.md +0 -27
  182. package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +0 -10
  183. package/skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py +0 -16
  184. package/skills/doc-gist/scripts/gist_upload.py +0 -177
  185. package/skills/doc-gist/scripts/test_gist_upload.py +0 -51
  186. /package/skills/{doc-gist/scripts/doc_gist_scripts_constants → log-audit/scripts/log_audit_constants}/__init__.py +0 -0
@@ -0,0 +1,197 @@
1
+ """Behavioural checks for the show-don't-tell narrative gate.
2
+
3
+ A readable summary earns its place by painting a concrete scene: a sample
4
+ input, an annotated outcome, an OK/FLAG contrast a reader pictures at once. A
5
+ summary that runs many short sentences with no worked example tells without
6
+ showing. The gate flags that wall in a top-level, class, or callable summary,
7
+ and stays quiet the moment the narrative carries a '::' listing or a '>>>'
8
+ doctest.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import importlib.util
14
+ import sys
15
+ from pathlib import Path
16
+ from types import ModuleType
17
+
18
+ _HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
19
+ if _HOOKS_DIRECTORY not in sys.path:
20
+ sys.path.insert(0, _HOOKS_DIRECTORY)
21
+
22
+
23
+ def _load_enforcer_module() -> ModuleType:
24
+ enforcer_module_name = "code_rules_enforcer"
25
+ enforcer_path = Path(__file__).parent / (enforcer_module_name + ".py")
26
+ spec = importlib.util.spec_from_file_location(enforcer_module_name, enforcer_path)
27
+ assert spec is not None
28
+ assert spec.loader is not None
29
+ loaded_module = importlib.util.module_from_spec(spec)
30
+ spec.loader.exec_module(loaded_module)
31
+ return loaded_module
32
+
33
+
34
+ code_rules_enforcer = _load_enforcer_module()
35
+
36
+
37
+ def check_docstring_prose_wall_without_illustration(content: str, file_path: str) -> list[str]:
38
+ return code_rules_enforcer.check_docstring_prose_wall_without_illustration(content, file_path)
39
+
40
+
41
+ def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
42
+ return code_rules_enforcer.validate_content(content, file_path, old_content)
43
+
44
+
45
+ PRODUCTION_FILE_PATH = "/project/src/tally.py"
46
+ SUITE_FILE_PATH = "/project/src/test_tally.py"
47
+ INFRA_PATH = "/home/user/.claude/hooks/blocking/example.py"
48
+
49
+
50
+ def _fixture_top_level_wall() -> str:
51
+ return (
52
+ '"""Assemble the nightly voyage tally from the harbor scans.\n'
53
+ "\n"
54
+ "A scan names one vessel and where it dropped anchor.\n"
55
+ "The tally walks the scans in arrival order and keeps that order.\n"
56
+ "A calm voyage ends well for every vessel it carried.\n"
57
+ "A halted voyage marks the vessel it was near when the storm arrived.\n"
58
+ "A wrecked voyage marks the vessel that sank and stops the walk there.\n"
59
+ "The tally groups the vessels by their final port for the harbor.\n"
60
+ "The harbor reads the tally and sees every arrival at a glance.\n"
61
+ '"""\n'
62
+ "\n"
63
+ "def emit_tally() -> None:\n"
64
+ " return None\n"
65
+ )
66
+
67
+
68
+ def _fixture_callable_wall() -> str:
69
+ return (
70
+ "def emit_tally() -> None:\n"
71
+ ' """Post the voyage tally to the harbor board.\n'
72
+ "\n"
73
+ " A scan names one vessel and where it dropped anchor.\n"
74
+ " The tally walks the scans in arrival order and keeps that order.\n"
75
+ " A calm voyage ends well for every vessel it carried.\n"
76
+ " A halted voyage marks the vessel it was near when the storm arrived.\n"
77
+ " A wrecked voyage marks the vessel that sank and stops the walk there.\n"
78
+ " The tally groups the vessels by their final port for the reader.\n"
79
+ " The reader reads the whole tally at a single glance.\n"
80
+ ' """\n'
81
+ " return None\n"
82
+ )
83
+
84
+
85
+ def _fixture_wall_with_listing() -> str:
86
+ return (
87
+ "def emit_tally() -> None:\n"
88
+ ' """Post the voyage tally to the harbor board.\n'
89
+ "\n"
90
+ " A scan names one vessel and where it dropped anchor::\n"
91
+ "\n"
92
+ " vessel 42 -- halted -> marked mid-voyage\n"
93
+ " OK: a calm voyage ends well for every vessel\n"
94
+ " FLAG: a wrecked voyage reads the same as a calm one\n"
95
+ "\n"
96
+ " The reader reads the whole tally at a single glance.\n"
97
+ ' """\n'
98
+ " return None\n"
99
+ )
100
+
101
+
102
+ def _fixture_wall_with_doctest() -> str:
103
+ return (
104
+ "def emit_tally() -> None:\n"
105
+ ' """Post the voyage tally to the harbor board.\n'
106
+ "\n"
107
+ " A calm voyage ends well for every vessel it carried.\n"
108
+ " A halted voyage marks the vessel it was near when the storm arrived.\n"
109
+ " A wrecked voyage marks the vessel that sank and stops the walk there.\n"
110
+ "\n"
111
+ " >>> emit_tally()\n"
112
+ " vessel 42 halted\n"
113
+ " vessel 43 calm\n"
114
+ "\n"
115
+ " The reader reads the whole tally at a single glance.\n"
116
+ ' """\n'
117
+ " return None\n"
118
+ )
119
+
120
+
121
+ def _fixture_short_summary() -> str:
122
+ return (
123
+ "def emit_tally() -> None:\n"
124
+ ' """Post the voyage tally to the harbor board.\n'
125
+ "\n"
126
+ " A scan names one vessel and where it dropped anchor.\n"
127
+ " A calm voyage ends well for every vessel it carried.\n"
128
+ " A halted voyage marks the vessel it was near when it stopped.\n"
129
+ " The reader reads the whole tally at a single glance.\n"
130
+ ' """\n'
131
+ " return None\n"
132
+ )
133
+
134
+
135
+ def test_should_flag_prose_wall_with_no_illustration() -> None:
136
+ issues = check_docstring_prose_wall_without_illustration(
137
+ _fixture_top_level_wall(), PRODUCTION_FILE_PATH
138
+ )
139
+ assert any("worked example" in each for each in issues), (
140
+ f"A top-level wall must flag, got: {issues!r}"
141
+ )
142
+ assert any("module" in each for each in issues)
143
+ assert len(issues) == 1
144
+
145
+
146
+ def test_should_flag_function_prose_wall() -> None:
147
+ issues = check_docstring_prose_wall_without_illustration(
148
+ _fixture_callable_wall(), PRODUCTION_FILE_PATH
149
+ )
150
+ assert any("emit_tally" in each for each in issues), (
151
+ f"A callable wall must flag, got: {issues!r}"
152
+ )
153
+ assert len(issues) == 1
154
+
155
+
156
+ def test_should_not_flag_wall_with_literal_block() -> None:
157
+ issues = check_docstring_prose_wall_without_illustration(
158
+ _fixture_wall_with_listing(), PRODUCTION_FILE_PATH
159
+ )
160
+ assert issues == [], f"A '::' listing must pass, got: {issues!r}"
161
+
162
+
163
+ def test_should_not_flag_wall_with_doctest() -> None:
164
+ issues = check_docstring_prose_wall_without_illustration(
165
+ _fixture_wall_with_doctest(), PRODUCTION_FILE_PATH
166
+ )
167
+ assert issues == [], f"A '>>>' doctest must pass, got: {issues!r}"
168
+
169
+
170
+ def test_should_not_flag_short_narrative() -> None:
171
+ issues = check_docstring_prose_wall_without_illustration(
172
+ _fixture_short_summary(), PRODUCTION_FILE_PATH
173
+ )
174
+ assert issues == [], f"A summary at the sentence limit must pass, got: {issues!r}"
175
+
176
+
177
+ def test_should_skip_test_file() -> None:
178
+ issues = check_docstring_prose_wall_without_illustration(_fixture_top_level_wall(), SUITE_FILE_PATH)
179
+ assert issues == [], f"A suite path stays exempt, got: {issues!r}"
180
+
181
+
182
+ def test_should_skip_hook_infrastructure() -> None:
183
+ issues = check_docstring_prose_wall_without_illustration(
184
+ _fixture_top_level_wall(), INFRA_PATH
185
+ )
186
+ assert issues == [], f"An infra path stays exempt, got: {issues!r}"
187
+
188
+
189
+ def test_should_handle_syntax_error_gracefully() -> None:
190
+ issues = check_docstring_prose_wall_without_illustration("def broken(\n", PRODUCTION_FILE_PATH)
191
+ assert issues == [], f"A syntax error yields no issues, got: {issues!r}"
192
+
193
+
194
+ def test_validate_content_surfaces_prose_wall() -> None:
195
+ issues = validate_content(_fixture_top_level_wall(), PRODUCTION_FILE_PATH, old_content="")
196
+ matching_issues = [each for each in issues if "worked example" in each]
197
+ assert matching_issues, f"validate_content must surface the wall, got: {issues!r}"
@@ -252,6 +252,33 @@ def test_should_flag_spaced_double_hyphen_joiner() -> None:
252
252
  assert len(issues) == 1
253
253
 
254
254
 
255
+ def _diagram_first_literal_block() -> str:
256
+ return (
257
+ "def emit_tally() -> None:\n"
258
+ ' """Post the voyage tally so the reader sees how the run ended.\n'
259
+ "\n"
260
+ " A calm voyage versus a halted one::\n"
261
+ "\n"
262
+ " vessel 42 -- halted -> marked mid-voyage while it sailed\n"
263
+ " calm — every vessel it carried shows a clean recorded outcome\n"
264
+ " OK: a calm run and a sunk run read differently in the log\n"
265
+ " FLAG: a sunk run looks identical to a calm finished run here\n"
266
+ " The reader reads the final outcome at a glance.\n"
267
+ ' """\n'
268
+ " return None\n"
269
+ )
270
+
271
+
272
+ def test_should_not_flag_diagram_first_literal_block() -> None:
273
+ issues = check_docstring_runon_sentence(
274
+ _diagram_first_literal_block(), PRODUCTION_FILE_PATH
275
+ )
276
+ assert issues == [], (
277
+ "A '::' listing's arrow, em-dash, and OK/FLAG rows sit outside the "
278
+ f"narrative and must not inflate the run-on count, got: {issues!r}"
279
+ )
280
+
281
+
255
282
  def test_docstring_names_every_inspected_section_header() -> None:
256
283
  docstring = code_rules_enforcer.check_docstring_runon_sentence.__doc__
257
284
  assert docstring is not None
@@ -181,6 +181,30 @@ def test_utcfromtimestamp_remediation_uses_generic_placeholder() -> None:
181
181
  assert "stamp, tz=" not in issues[0]
182
182
 
183
183
 
184
+ def test_utcnow_remediation_is_import_agnostic() -> None:
185
+ content = (
186
+ "import datetime\n"
187
+ "def stamp() -> object:\n"
188
+ " return datetime.datetime.utcnow()\n"
189
+ )
190
+ issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
191
+ assert len(issues) == 1
192
+ assert "use datetime.now(" not in issues[0]
193
+ assert "on the same datetime class" in issues[0]
194
+
195
+
196
+ def test_utcfromtimestamp_remediation_is_import_agnostic() -> None:
197
+ content = (
198
+ "import datetime\n"
199
+ "def build(stamp: float) -> object:\n"
200
+ " return datetime.datetime.utcfromtimestamp(stamp)\n"
201
+ )
202
+ issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
203
+ assert len(issues) == 1
204
+ assert "use datetime.fromtimestamp(" not in issues[0]
205
+ assert "on the same datetime class" in issues[0]
206
+
207
+
184
208
  def test_remediation_does_not_mandate_astimezone() -> None:
185
209
  content = (
186
210
  "from datetime import datetime\n"
@@ -177,6 +177,39 @@ def test_advisory_should_flag_outer_constants_after_nested_def() -> None:
177
177
  )
178
178
 
179
179
 
180
+ def test_scan_function_body_constants_skips_docstring_contrast_line() -> None:
181
+ source = (
182
+ "def demo(label):\n"
183
+ ' """Flag a boolean assignment whose target and callee clash.\n'
184
+ "\n"
185
+ " ::\n"
186
+ "\n"
187
+ " FLAG: ready = make_stale(...)\n"
188
+ ' """\n'
189
+ " return label\n"
190
+ )
191
+ advisory_issues = code_rules_enforcer._scan_function_body_constants(source)
192
+ assert advisory_issues == [], (
193
+ f"Expected no function-local constant issue on an illustrative row, got: {advisory_issues}"
194
+ )
195
+
196
+
197
+ def test_scan_function_body_constants_still_flags_real_function_body_constant() -> None:
198
+ source = (
199
+ "def demo(label):\n"
200
+ ' """Flag a boolean assignment whose target and callee clash.\n'
201
+ "\n"
202
+ " FLAG: ready = make_stale(...)\n"
203
+ ' """\n'
204
+ " SOME_CONST = 3\n"
205
+ " return SOME_CONST\n"
206
+ )
207
+ advisory_issues = code_rules_enforcer._scan_function_body_constants(source)
208
+ assert any("SOME_CONST" in issue for issue in advisory_issues), (
209
+ f"Expected the real function-body constant SOME_CONST to stay flagged, got: {advisory_issues}"
210
+ )
211
+
212
+
180
213
  def test_check_constants_outside_config_flags_annotated_assignment() -> None:
181
214
  source = "TEXT_FILE_ENCODING: str = 'utf-8'\n"
182
215
  issues = code_rules_enforcer.check_constants_outside_config(
@@ -18,14 +18,37 @@ from code_rules_enforcer import ( # noqa: E402
18
18
  )
19
19
  from code_rules_shared import ( # noqa: E402
20
20
  changed_line_numbers,
21
+ docstring_line_numbers,
21
22
  )
22
23
 
23
24
  code_rules_enforcer = SimpleNamespace(
24
25
  changed_line_numbers=changed_line_numbers,
26
+ docstring_line_numbers=docstring_line_numbers,
25
27
  prior_and_post_edit_content=prior_and_post_edit_content,
26
28
  )
27
29
 
28
30
 
31
+ def test_docstring_line_numbers_spans_a_multi_line_function_docstring() -> None:
32
+ source = (
33
+ "def describe():\n"
34
+ ' """First row.\n'
35
+ "\n"
36
+ " Third row.\n"
37
+ ' """\n'
38
+ " return 1\n"
39
+ )
40
+ assert code_rules_enforcer.docstring_line_numbers(source) == {2, 3, 4, 5}
41
+
42
+
43
+ def test_docstring_line_numbers_excludes_real_code_lines() -> None:
44
+ source = "def compute():\n counter = 41\n return counter\n"
45
+ assert code_rules_enforcer.docstring_line_numbers(source) == set()
46
+
47
+
48
+ def test_docstring_line_numbers_returns_empty_set_on_syntax_error() -> None:
49
+ assert code_rules_enforcer.docstring_line_numbers("def broken(:\n") == set()
50
+
51
+
29
52
  def test_readable_prior_yields_consistent_prior_and_reconstruction(tmp_path) -> None:
30
53
  """When the prior reads cleanly, the helper returns the same prior content it
31
54
  reconstructed the post-edit view from, so the two never diverge across two
@@ -0,0 +1,54 @@
1
+ """Behavior tests for the code_rules_magic_values check module.
2
+
3
+ A diagram-first docstring may carry a bare number on an illustrative row
4
+ (``at line 2``). That number is prose, not a code literal, so the magic-value
5
+ check skips docstring rows while still flagging a real magic number in an
6
+ actual code statement.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ _BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
15
+ _HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
16
+ if _BLOCKING_DIRECTORY not in sys.path:
17
+ sys.path.insert(0, _BLOCKING_DIRECTORY)
18
+ if _HOOKS_DIRECTORY not in sys.path:
19
+ sys.path.insert(0, _HOOKS_DIRECTORY)
20
+
21
+ from code_rules_magic_values import check_magic_values # noqa: E402
22
+
23
+ PRODUCTION_FILE_PATH = "packages/app/rendering.py"
24
+
25
+
26
+ def test_magic_value_check_skips_bare_number_inside_docstring_illustration() -> None:
27
+ source = (
28
+ "def summarize(record):\n"
29
+ ' """Summarize how a run ended.\n'
30
+ "\n"
31
+ " A finished run shows a clean outcome::\n"
32
+ "\n"
33
+ " marked in-flight at row 2\n"
34
+ ' """\n'
35
+ " return record\n"
36
+ )
37
+ issues = check_magic_values(source, PRODUCTION_FILE_PATH)
38
+ assert issues == [], f"Expected no magic-value issue on an illustrative row, got: {issues}"
39
+
40
+
41
+ def test_magic_value_check_still_flags_real_magic_number_in_code_statement() -> None:
42
+ source = (
43
+ "def summarize(record):\n"
44
+ ' """Summarize how a run ended.\n'
45
+ "\n"
46
+ " marked in-flight at row 2\n"
47
+ ' """\n'
48
+ " threshold = record.score * 42\n"
49
+ " return threshold\n"
50
+ )
51
+ issues = check_magic_values(source, PRODUCTION_FILE_PATH)
52
+ assert any(issue.endswith("Magic value 42 - extract to named constant") for issue in issues), (
53
+ f"Expected the real magic number 42 to stay flagged, got: {issues}"
54
+ )
@@ -142,12 +142,6 @@ def test_path_exempts_existing_session_env_cleanup_definition_site() -> None:
142
142
  assert path_is_exempt("packages/claude-dev-env/hooks/session/session_env_cleanup.py")
143
143
 
144
144
 
145
- def test_path_exempts_existing_md_to_html_test_support_definition_site() -> None:
146
- assert path_is_exempt(
147
- "packages/claude-dev-env/hooks/blocking/_md_to_html_blocker_test_support.py"
148
- )
149
-
150
-
151
145
  def test_path_exempts_existing_teardown_worktrees_definition_site() -> None:
152
146
  assert path_is_exempt(
153
147
  "packages/claude-dev-env/skills/_shared/pr-loop/scripts/teardown_worktrees.py"
@@ -0,0 +1,184 @@
1
+ """Unit tests: persistent-agent state keys leave both pr-converge hooks unchanged.
2
+
3
+ pr-converge-state.json carries `agents_session_id` and `persistent_agents`
4
+ (the persistent per-step agent map with a `fix_executor` entry). Neither the
5
+ enforcer (`pr_converge_bugteam_enforcer`) nor the tracker
6
+ (`pr_converge_bugteam_skill_tracker`) reads these keys, so their allow/block
7
+ decisions and stamped fields match a state file without them, and the tracker
8
+ preserves the keys unchanged on its stamp write.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import importlib.util
14
+ import io
15
+ import json
16
+ import pathlib
17
+ import sys
18
+ from types import ModuleType
19
+ from typing import Any
20
+ from unittest import mock
21
+
22
+ import pytest
23
+
24
+ _BLOCKING_DIR = pathlib.Path(__file__).parent
25
+ _HOOKS_TREE = _BLOCKING_DIR.parent
26
+ _LIFECYCLE_DIR = _HOOKS_TREE / "lifecycle"
27
+ for each_path in (str(_BLOCKING_DIR), str(_HOOKS_TREE)):
28
+ if each_path not in sys.path:
29
+ sys.path.insert(0, each_path)
30
+
31
+
32
+ def _load_hook_module(module_name: str, module_path: pathlib.Path) -> ModuleType:
33
+ hook_spec = importlib.util.spec_from_file_location(module_name, module_path)
34
+ assert hook_spec is not None
35
+ assert hook_spec.loader is not None
36
+ loaded_module = importlib.util.module_from_spec(hook_spec)
37
+ hook_spec.loader.exec_module(loaded_module)
38
+ return loaded_module
39
+
40
+
41
+ enforcer_module = _load_hook_module(
42
+ "pr_converge_bugteam_enforcer_state_tolerance_target",
43
+ _BLOCKING_DIR / "pr_converge_bugteam_enforcer.py",
44
+ )
45
+ tracker_module = _load_hook_module(
46
+ "pr_converge_bugteam_skill_tracker_state_tolerance_target",
47
+ _LIFECYCLE_DIR / "pr_converge_bugteam_skill_tracker.py",
48
+ )
49
+
50
+ from hooks_constants.pr_converge_bugteam_enforcer_constants import (
51
+ BUGTEAM_PHASE,
52
+ CLAUDE_JOB_DIR_ENV_VAR,
53
+ PR_CONVERGE_STATE_FILENAME,
54
+ )
55
+
56
+ _HEAD_SHA_CURRENT = "abc123def456abc123def456abc123def456abcd"
57
+ _TICK_CURRENT = 7
58
+ _AUDIT_PROMPT = "Run the bugteam audit and report findings against the A-J categories."
59
+ _AGENTS_SESSION_ID = "session-0123456789abcdef"
60
+ _FIX_EXECUTOR_AGENT_ID = "prc-fix-42"
61
+
62
+
63
+ def _persistent_agent_state_keys() -> dict[str, Any]:
64
+ return {
65
+ "agents_session_id": _AGENTS_SESSION_ID,
66
+ "persistent_agents": {
67
+ "fix_executor": {
68
+ "agent_id": _FIX_EXECUTOR_AGENT_ID,
69
+ "created_tick": 2,
70
+ "last_used_tick": 6,
71
+ }
72
+ },
73
+ }
74
+
75
+
76
+ def _bugteam_phase_state(**overrides: Any) -> dict[str, Any]:
77
+ baseline_state: dict[str, Any] = {
78
+ "phase": BUGTEAM_PHASE,
79
+ "current_head": _HEAD_SHA_CURRENT,
80
+ "tick_count": _TICK_CURRENT,
81
+ "bugteam_skill_invoked_at_head": None,
82
+ "bugteam_skill_invoked_at_tick": None,
83
+ }
84
+ baseline_state.update(_persistent_agent_state_keys())
85
+ baseline_state.update(overrides)
86
+ return baseline_state
87
+
88
+
89
+ def _write_state(state_directory: pathlib.Path, state: dict[str, Any]) -> pathlib.Path:
90
+ state_path = state_directory / PR_CONVERGE_STATE_FILENAME
91
+ state_path.write_text(json.dumps(state), encoding="utf-8")
92
+ return state_path
93
+
94
+
95
+ def _read_state(state_directory: pathlib.Path) -> dict[str, Any]:
96
+ state_path = state_directory / PR_CONVERGE_STATE_FILENAME
97
+ return json.loads(state_path.read_text(encoding="utf-8"))
98
+
99
+
100
+ def _clean_coder_audit_payload() -> dict[str, Any]:
101
+ return {
102
+ "tool_name": "Agent",
103
+ "tool_input": {
104
+ "subagent_type": "clean-coder",
105
+ "prompt": _AUDIT_PROMPT,
106
+ "description": "Audit the PR",
107
+ },
108
+ }
109
+
110
+
111
+ def _bugteam_skill_payload() -> dict[str, Any]:
112
+ return {
113
+ "tool_name": "Skill",
114
+ "tool_input": {"skill": "bugteam", "args": "https://github.com/o/r/pull/1"},
115
+ }
116
+
117
+
118
+ def _run_enforcer_main_with_io(input_text: str) -> str:
119
+ with mock.patch("sys.stdin", io.StringIO(input_text)):
120
+ with mock.patch("sys.stdout", new_callable=io.StringIO) as captured_stdout:
121
+ try:
122
+ enforcer_module.main()
123
+ except SystemExit:
124
+ pass
125
+ return captured_stdout.getvalue()
126
+
127
+
128
+ def _run_tracker_main_with_io(input_text: str) -> None:
129
+ with mock.patch("sys.stdin", io.StringIO(input_text)):
130
+ with mock.patch("sys.stdout", new_callable=io.StringIO):
131
+ with mock.patch("sys.stderr", new_callable=io.StringIO):
132
+ try:
133
+ tracker_module.main()
134
+ except SystemExit:
135
+ pass
136
+
137
+
138
+ @pytest.fixture()
139
+ def claude_job_directory(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
140
+ monkeypatch.setenv(CLAUDE_JOB_DIR_ENV_VAR, str(tmp_path))
141
+ return tmp_path
142
+
143
+
144
+ def test_should_block_enforcer_when_persistent_agent_keys_present_without_skill(
145
+ claude_job_directory: pathlib.Path,
146
+ ) -> None:
147
+ _write_state(claude_job_directory, _bugteam_phase_state())
148
+ captured_output = _run_enforcer_main_with_io(json.dumps(_clean_coder_audit_payload()))
149
+ deny_payload = json.loads(captured_output)
150
+ assert deny_payload["hookSpecificOutput"]["permissionDecision"] == "deny"
151
+
152
+
153
+ def test_should_allow_enforcer_when_persistent_agent_keys_present_with_skill_recorded(
154
+ claude_job_directory: pathlib.Path,
155
+ ) -> None:
156
+ _write_state(
157
+ claude_job_directory,
158
+ _bugteam_phase_state(
159
+ bugteam_skill_invoked_at_head=_HEAD_SHA_CURRENT,
160
+ bugteam_skill_invoked_at_tick=_TICK_CURRENT,
161
+ ),
162
+ )
163
+ captured_output = _run_enforcer_main_with_io(json.dumps(_clean_coder_audit_payload()))
164
+ assert captured_output == ""
165
+
166
+
167
+ def test_should_stamp_tracker_fields_when_persistent_agent_keys_present(
168
+ claude_job_directory: pathlib.Path,
169
+ ) -> None:
170
+ _write_state(claude_job_directory, _bugteam_phase_state())
171
+ _run_tracker_main_with_io(json.dumps(_bugteam_skill_payload()))
172
+ updated_state = _read_state(claude_job_directory)
173
+ assert updated_state["bugteam_skill_invoked_at_head"] == _HEAD_SHA_CURRENT
174
+ assert updated_state["bugteam_skill_invoked_at_tick"] == _TICK_CURRENT
175
+
176
+
177
+ def test_should_preserve_persistent_agent_keys_on_tracker_stamp_write(
178
+ claude_job_directory: pathlib.Path,
179
+ ) -> None:
180
+ _write_state(claude_job_directory, _bugteam_phase_state())
181
+ _run_tracker_main_with_io(json.dumps(_bugteam_skill_payload()))
182
+ updated_state = _read_state(claude_job_directory)
183
+ assert updated_state["agents_session_id"] == _AGENTS_SESSION_ID
184
+ assert updated_state["persistent_agents"] == _persistent_agent_state_keys()["persistent_agents"]
@@ -10,11 +10,11 @@ each real hook script through its production ``__main__`` stdin path over a
10
10
  corpus that pins those fail-soft edges plus a representative allow payload, so a
11
11
  conversion that changes any decision is caught.
12
12
 
13
- The deterministic deny payloads for the two Write/Edit blockers whose triggers
14
- need no filesystem or state setup (``md_to_html_blocker``,
15
- ``open_questions_in_plans_blocker``) are exercised here too; each remaining
16
- hook's full deny coverage stays in its own suite, which also drives the real
17
- ``main()`` and so re-proves the decision after the parser swap.
13
+ The deterministic deny payload for the Write/Edit blocker whose trigger
14
+ needs no filesystem or state setup (``open_questions_in_plans_blocker``) is
15
+ exercised here too; each remaining hook's full deny coverage stays in its own
16
+ suite, which also drives the real ``main()`` and so re-proves the decision
17
+ after the parser swap.
18
18
  """
19
19
 
20
20
  import json
@@ -28,7 +28,6 @@ import pytest
28
28
  _BLOCKING_DIRECTORY = Path(__file__).resolve().parent
29
29
 
30
30
  ALL_CONVERTED_HOOK_FILENAMES = (
31
- "md_to_html_blocker.py",
32
31
  "open_questions_in_plans_blocker.py",
33
32
  "claude_md_orphan_file_blocker.py",
34
33
  "pr_converge_bugteam_enforcer.py",
@@ -83,30 +82,6 @@ def test_fail_soft_payload_exits_zero_with_no_decision(hook_filename: str, stdin
83
82
  )
84
83
 
85
84
 
86
- def test_md_to_html_blocker_still_denies_relative_markdown_write() -> None:
87
- payload = json.dumps(
88
- {
89
- "tool_name": "Write",
90
- "tool_input": {"file_path": "notes/topic.md", "content": "# Topic"},
91
- }
92
- )
93
- completed = _run_hook_script("md_to_html_blocker.py", payload)
94
- assert completed.returncode == 0
95
- assert _decision_from_stdout(completed) == "deny"
96
-
97
-
98
- def test_md_to_html_blocker_still_allows_non_markdown_write() -> None:
99
- payload = json.dumps(
100
- {
101
- "tool_name": "Write",
102
- "tool_input": {"file_path": "notes/topic.txt", "content": "plain"},
103
- }
104
- )
105
- completed = _run_hook_script("md_to_html_blocker.py", payload)
106
- assert completed.returncode == 0
107
- assert _decision_from_stdout(completed) is None
108
-
109
-
110
85
  def test_open_questions_blocker_still_denies_plan_with_open_questions(
111
86
  tmp_path: Path,
112
87
  ) -> None:
@@ -24,7 +24,6 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
24
24
  | `dead_dataclass_field_constants.py` | Patterns for detecting unused dataclass fields |
25
25
  | `dead_module_constant_constants.py` | Patterns for detecting unexported `UPPER_SNAKE` constants in `*_constants.py` modules |
26
26
  | `destructive_command_segment_constants.py` | The list of destructive shell command patterns the blocker matches |
27
- | `doc_gist_auto_publish_constants.py` | Sentinel marker and URL patterns for the doc-gist auto-publish hook |
28
27
  | `docstring_rule_gate_count_blocker_constants.py` | Target rule basename, spelled-out-number lookup, count-clause and `check_*` validator patterns, and block-message text for the docstring-rule gate-count staleness blocker |
29
28
  | `duplicate_function_body_constants.py` | Hashing and comparison config for the duplicate-body check |
30
29
  | `duplicate_rmtree_helper_blocker_constants.py` | Sanctioned Windows-safe rmtree helper names, the definition pattern, and the exempt-path set for the duplicate-rmtree-helper blocker |
@@ -34,10 +33,8 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
34
33
  | `hook_block_logger.py` | `log_hook_block()` — shared fail-safe logger every blocking hook calls to append a JSON record of each block decision to `~/.claude/logs/hook-blocks.log` |
35
34
  | `hook_log_extractor_constants.py` | Neon table name, offset state file path, timeouts, and outcome-type mapping for the hook-log extractor |
36
35
  | `hook_prose_detector_consistency_constants.py` | Trigger patterns and corrective messages for the hook-prose consistency checker |
37
- | `html_companion_constants.py` | Blocked URL schemes and other config for the `.md`-to-`.html` companion hook |
38
36
  | `inline_tuple_string_magic_constants.py` | Patterns for detecting magic strings in inline tuple literals |
39
37
  | `js_conventions_constants.py` | Banned identifier set, boolean-prefix pattern, and declaration/JSDoc patterns for the JavaScript convention checks |
40
- | `md_to_html_blocker_constants.py` | Path exemptions and trigger patterns for the markdown-to-html blocker |
41
38
  | `messages.py` | Short user-facing notice strings shown when a Stop hook redirects agent behavior |
42
39
  | `multi_edit_reconstruction.py` | `apply_edits()` / `edits_for_tool()` — shared helpers that reconstruct the post-edit content of an Edit or MultiEdit, imported by the blockers that judge post-edit content |
43
40
  | `open_questions_in_plans_blocker_constants.py` | Patterns for detecting unresolved open questions in plan documents |