claude-dev-env 1.82.0 → 1.84.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 (217) hide show
  1. package/CLAUDE.md +16 -13
  2. package/_shared/pr-loop/audit-contract.md +24 -12
  3. package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  4. package/_shared/pr-loop/scripts/README.md +1 -0
  5. package/_shared/pr-loop/scripts/_claude_permissions_common.py +16 -5
  6. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
  8. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviewer_availability_constants.py +12 -0
  9. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +0 -2
  10. package/_shared/pr-loop/scripts/reviewer_availability.py +182 -0
  11. package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
  12. package/_shared/pr-loop/scripts/terminology_sweep.py +9 -33
  13. package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
  14. package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +212 -0
  15. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +18 -0
  16. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +18 -0
  17. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +37 -0
  18. package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +159 -0
  19. package/_shared/pr-loop/scripts/tests/test_reviewer_availability_constants.py +36 -0
  20. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +14 -0
  21. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +14 -4
  22. package/agents/clean-coder.md +3 -0
  23. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  24. package/bin/install.mjs +16 -8
  25. package/bin/install.test.mjs +17 -6
  26. package/commands/CLAUDE.md +0 -1
  27. package/docs/CODE_RULES.md +1 -1
  28. package/hooks/blocking/CLAUDE.md +2 -4
  29. package/hooks/blocking/code_rules_constants_config.py +164 -1
  30. package/hooks/blocking/code_rules_docstrings.py +428 -15
  31. package/hooks/blocking/code_rules_enforcer.py +33 -0
  32. package/hooks/blocking/code_rules_imports_logging.py +867 -1
  33. package/hooks/blocking/code_rules_magic_values.py +5 -0
  34. package/hooks/blocking/code_rules_naming_collection.py +152 -6
  35. package/hooks/blocking/code_rules_shared.py +34 -0
  36. package/hooks/blocking/code_rules_string_magic.py +68 -0
  37. package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
  38. package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
  39. package/hooks/blocking/reviewer_spawn_gate.py +182 -0
  40. package/hooks/blocking/stale_comment_reference_blocker.py +267 -0
  41. package/hooks/blocking/state_description_blocker.py +96 -5
  42. package/hooks/blocking/test_code_rules_config_duplicate_path_anchor.py +132 -0
  43. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +2 -0
  44. package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +385 -0
  45. package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +197 -0
  46. package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +27 -0
  47. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +67 -0
  48. package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +40 -0
  49. package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +213 -0
  50. package/hooks/blocking/test_code_rules_enforcer_referenced_underscore_loop.py +169 -0
  51. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +33 -0
  52. package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -0
  53. package/hooks/blocking/test_code_rules_js_bare_flag_return_directive.py +266 -0
  54. package/hooks/blocking/test_code_rules_js_sibling_return_object_key_drift.py +490 -0
  55. package/hooks/blocking/test_code_rules_logging_adjacent_literals.py +171 -0
  56. package/hooks/blocking/test_code_rules_magic_values.py +54 -0
  57. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +0 -6
  58. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +184 -0
  59. package/hooks/blocking/test_pre_tool_use_dispatcher.py +9 -3
  60. package/hooks/blocking/test_reviewer_spawn_gate.py +230 -0
  61. package/hooks/blocking/test_shared_stdin_adoption.py +5 -30
  62. package/hooks/blocking/test_stale_comment_reference_blocker.py +236 -0
  63. package/hooks/blocking/test_state_description_blocker.py +135 -0
  64. package/hooks/hooks.json +5 -0
  65. package/hooks/hooks_constants/CLAUDE.md +3 -4
  66. package/hooks/hooks_constants/blocking_check_limits.py +46 -0
  67. package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -0
  68. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  69. package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +0 -1
  70. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -5
  71. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  72. package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +41 -0
  73. package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +76 -0
  74. package/hooks/hooks_constants/state_description_blocker_constants.py +8 -0
  75. package/hooks/validation/post_tool_use_dispatcher.py +1 -1
  76. package/hooks/validation/test_post_tool_use_dispatcher.py +51 -35
  77. package/hooks/workflow/CLAUDE.md +2 -8
  78. package/package.json +1 -1
  79. package/rules/CLAUDE.md +5 -2
  80. package/rules/claude-md-orphan-file.md +5 -0
  81. package/rules/docstring-prose-matches-implementation.md +10 -1
  82. package/rules/env-var-table-code-drift.md +5 -0
  83. package/rules/es-exe-file-search.md +17 -0
  84. package/rules/no-historical-clutter.md +12 -1
  85. package/rules/orphan-css-class.md +5 -0
  86. package/rules/package-inventory-stale-entry.md +10 -0
  87. package/rules/paired-test-coverage.md +5 -0
  88. package/rules/plain-illustrative-docstrings.md +40 -1
  89. package/rules/verify-before-asking.md +7 -0
  90. package/rules/verify-runtime-state.md +40 -0
  91. package/rules/windows-filesystem-safe.md +8 -0
  92. package/rules/workers-done-before-complete.md +33 -0
  93. package/rules/workflow-substitution-slots.md +5 -0
  94. package/skills/CLAUDE.md +7 -2
  95. package/skills/autoconverge/SKILL.md +48 -61
  96. package/skills/autoconverge/reference/closing-report.md +6 -6
  97. package/skills/autoconverge/reference/convergence.md +17 -15
  98. package/skills/autoconverge/reference/gotchas.md +6 -3
  99. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +2 -9
  100. package/skills/autoconverge/workflow/converge.contract.test.mjs +153 -10
  101. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +55 -19
  102. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
  103. package/skills/autoconverge/workflow/converge.mjs +167 -106
  104. package/skills/autoconverge/workflow/render_report.py +7 -11
  105. package/skills/bugteam/CLAUDE.md +1 -1
  106. package/skills/bugteam/PROMPTS.md +7 -6
  107. package/skills/bugteam/SKILL.md +25 -72
  108. package/skills/bugteam/reference/CLAUDE.md +1 -3
  109. package/skills/bugteam/reference/README.md +1 -1
  110. package/skills/bugteam/reference/audit-and-teammates.md +1 -1
  111. package/skills/bugteam/reference/obstacles/fix-publish-summary.md +1 -1
  112. package/skills/bugteam/reference/team-setup.md +8 -7
  113. package/skills/bugteam/scripts/CLAUDE.md +0 -6
  114. package/skills/bugteam/scripts/README.md +0 -4
  115. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
  116. package/skills/code/SKILL.md +2 -0
  117. package/skills/copilot-review/CLAUDE.md +1 -1
  118. package/skills/copilot-review/SKILL.md +25 -23
  119. package/skills/everything-search/SKILL.md +5 -0
  120. package/skills/findbugs/CLAUDE.md +2 -2
  121. package/skills/findbugs/SKILL.md +22 -83
  122. package/skills/fixbugs/SKILL.md +2 -4
  123. package/skills/log-audit/CLAUDE.md +20 -0
  124. package/skills/log-audit/SKILL.md +68 -0
  125. package/skills/log-audit/reference/CLAUDE.md +9 -0
  126. package/skills/log-audit/reference/charter.md +52 -0
  127. package/skills/log-audit/scripts/CLAUDE.md +27 -0
  128. package/skills/log-audit/scripts/cluster_recurrences.py +261 -0
  129. package/skills/log-audit/scripts/collect_log_window.py +199 -0
  130. package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +12 -0
  131. package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +23 -0
  132. package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +24 -0
  133. package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +49 -0
  134. package/skills/log-audit/scripts/mine_copilot_findings.py +302 -0
  135. package/skills/log-audit/scripts/test_cluster_recurrences.py +160 -0
  136. package/skills/log-audit/scripts/test_collect_log_window.py +111 -0
  137. package/skills/log-audit/scripts/test_mine_copilot_findings.py +126 -0
  138. package/skills/monitor-open-prs/SKILL.md +2 -2
  139. package/skills/post-audit-findings/SKILL.md +84 -0
  140. package/skills/pr-converge/CLAUDE.md +2 -0
  141. package/skills/pr-converge/SKILL.md +72 -59
  142. package/skills/pr-converge/reference/CLAUDE.md +1 -1
  143. package/skills/pr-converge/reference/convergence-gates.md +16 -19
  144. package/skills/pr-converge/reference/examples.md +5 -5
  145. package/skills/pr-converge/reference/fix-protocol.md +16 -43
  146. package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +1 -1
  147. package/skills/pr-converge/reference/per-tick.md +24 -45
  148. package/skills/pr-converge/reference/state-schema.md +15 -0
  149. package/skills/pr-converge/scripts/README.md +3 -5
  150. package/skills/pr-fix-protocol/SKILL.md +70 -0
  151. package/skills/pr-loop-lifecycle/SKILL.md +73 -0
  152. package/skills/{bugteam → pr-loop-lifecycle}/reference/teardown-publish-permissions.md +22 -24
  153. package/skills/pr-scope-resolve/SKILL.md +48 -0
  154. package/skills/qbug/CLAUDE.md +4 -4
  155. package/skills/qbug/SKILL.md +46 -144
  156. package/skills/qbug/test_qbug_skill_audit_schema.py +2 -2
  157. package/skills/qbug/test_qbug_skill_post_fix_audit.py +1 -1
  158. package/skills/reviewer-gates/SKILL.md +96 -0
  159. package/skills/session-log/CLAUDE.md +7 -7
  160. package/skills/session-log/SKILL.md +27 -44
  161. package/skills/test_markdown_link_integrity.py +103 -0
  162. package/commands/doc-gist.md +0 -16
  163. package/hooks/blocking/_md_to_html_blocker_test_support.py +0 -65
  164. package/hooks/blocking/conftest.py +0 -30
  165. package/hooks/blocking/md_path_exemptions.py +0 -224
  166. package/hooks/blocking/md_to_html_blocker.py +0 -155
  167. package/hooks/blocking/test_md_to_html_blocker_exemptions.py +0 -434
  168. package/hooks/blocking/test_md_to_html_blocker_extensions.py +0 -157
  169. package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +0 -336
  170. package/hooks/hooks_constants/doc_gist_auto_publish_constants.py +0 -18
  171. package/hooks/hooks_constants/html_companion_constants.py +0 -20
  172. package/hooks/hooks_constants/md_to_html_blocker_constants.py +0 -76
  173. package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +0 -125
  174. package/hooks/workflow/doc_gist_auto_publish.py +0 -144
  175. package/hooks/workflow/md_to_html_companion.py +0 -358
  176. package/hooks/workflow/test_doc_gist_auto_publish.py +0 -117
  177. package/hooks/workflow/test_md_to_html_companion.py +0 -613
  178. package/skills/bugteam/reference/audit-contract.md +0 -163
  179. package/skills/bugteam/scripts/_bugteam_permissions_common.py +0 -455
  180. package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +0 -69
  181. package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -280
  182. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -266
  183. package/skills/bugteam/scripts/test__bugteam_permissions_common.py +0 -160
  184. package/skills/bugteam/scripts/test_agent_config_carveout.py +0 -356
  185. package/skills/bugteam/scripts/test_bugteam_permissions_common.py +0 -140
  186. package/skills/doc-gist/CLAUDE.md +0 -25
  187. package/skills/doc-gist/SKILL.md +0 -97
  188. package/skills/doc-gist/references/CLAUDE.md +0 -9
  189. package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +0 -453
  190. package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +0 -515
  191. package/skills/doc-gist/references/examples/03-code-review-pr.html +0 -638
  192. package/skills/doc-gist/references/examples/04-code-understanding.html +0 -491
  193. package/skills/doc-gist/references/examples/05-design-system.html +0 -629
  194. package/skills/doc-gist/references/examples/06-component-variants.html +0 -605
  195. package/skills/doc-gist/references/examples/07-prototype-animation.html +0 -455
  196. package/skills/doc-gist/references/examples/08-prototype-interaction.html +0 -396
  197. package/skills/doc-gist/references/examples/09-slide-deck.html +0 -592
  198. package/skills/doc-gist/references/examples/10-svg-illustrations.html +0 -492
  199. package/skills/doc-gist/references/examples/11-status-report.html +0 -528
  200. package/skills/doc-gist/references/examples/12-incident-report.html +0 -596
  201. package/skills/doc-gist/references/examples/13-flowchart-diagram.html +0 -395
  202. package/skills/doc-gist/references/examples/14-research-feature-explainer.html +0 -381
  203. package/skills/doc-gist/references/examples/15-research-concept-explainer.html +0 -368
  204. package/skills/doc-gist/references/examples/16-implementation-plan.html +0 -702
  205. package/skills/doc-gist/references/examples/17-pr-writeup.html +0 -595
  206. package/skills/doc-gist/references/examples/18-editor-triage-board.html +0 -573
  207. package/skills/doc-gist/references/examples/19-editor-feature-flags.html +0 -663
  208. package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +0 -722
  209. package/skills/doc-gist/references/examples/21-decision-signoff.html +0 -546
  210. package/skills/doc-gist/references/examples/CLAUDE.md +0 -25
  211. package/skills/doc-gist/references/examples/README.md +0 -5
  212. package/skills/doc-gist/scripts/CLAUDE.md +0 -27
  213. package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +0 -10
  214. package/skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py +0 -16
  215. package/skills/doc-gist/scripts/gist_upload.py +0 -177
  216. package/skills/doc-gist/scripts/test_gist_upload.py +0 -51
  217. /package/skills/{doc-gist/scripts/doc_gist_scripts_constants → log-audit/scripts/log_audit_constants}/__init__.py +0 -0
@@ -0,0 +1,236 @@
1
+ """Tests for the stale_comment_reference_blocker hook."""
2
+
3
+ import json
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+ from unittest.mock import patch
8
+
9
+ _BLOCKING_DIR = str(Path(__file__).resolve().parent)
10
+ _HOOKS_ROOT = str(Path(__file__).resolve().parent.parent)
11
+ if _BLOCKING_DIR not in sys.path:
12
+ sys.path.insert(0, _BLOCKING_DIR)
13
+ if _HOOKS_ROOT not in sys.path:
14
+ sys.path.insert(0, _HOOKS_ROOT)
15
+
16
+ from stale_comment_reference_blocker import ( # noqa: E402
17
+ build_deny_payload,
18
+ evaluate,
19
+ )
20
+
21
+ from hooks_constants.hook_block_logger import _HOOK_BLOCKS_LOG_RELATIVE_PATH # noqa: E402
22
+
23
+ HOOK_SCRIPT_PATH = str(Path(__file__).resolve().parent / "stale_comment_reference_blocker.py")
24
+
25
+ PATCHED_SLEEP_LINE = (
26
+ " with patch('theme_exports.core.exporter.asyncio.sleep', new_callable=AsyncMock):\n"
27
+ )
28
+ PATCHED_POLL_LINE = " with patch('theme_exports.core.exporter.wait_for_overlay_clear', new_callable=AsyncMock):\n"
29
+ KEPT_COMMENT_LINE = " # Mock asyncio\n"
30
+
31
+
32
+ def _run_hook(tool_name: str, tool_input: dict) -> subprocess.CompletedProcess:
33
+ payload = json.dumps({"tool_name": tool_name, "tool_input": tool_input})
34
+ return subprocess.run(
35
+ [sys.executable, HOOK_SCRIPT_PATH],
36
+ input=payload,
37
+ capture_output=True,
38
+ text=True,
39
+ check=False,
40
+ )
41
+
42
+
43
+ def _write_module(tmp_path: Path, source_text: str) -> Path:
44
+ written_module_path = tmp_path / "sample_module.py"
45
+ written_module_path.write_text(source_text, encoding="utf-8")
46
+ return written_module_path
47
+
48
+
49
+ def test_blocks_edit_that_orphans_the_comment_above(tmp_path: Path) -> None:
50
+ """Rewriting the patched target under an unchanged comment naming it
51
+ is denied.
52
+ """
53
+ written_module_path = _write_module(
54
+ tmp_path,
55
+ "def launch_export() -> None:\n" + KEPT_COMMENT_LINE + PATCHED_SLEEP_LINE,
56
+ )
57
+ outcome = _run_hook(
58
+ "Edit",
59
+ {
60
+ "file_path": str(written_module_path),
61
+ "old_string": PATCHED_SLEEP_LINE.rstrip("\n"),
62
+ "new_string": PATCHED_POLL_LINE.rstrip("\n"),
63
+ },
64
+ )
65
+ assert outcome.returncode == 0
66
+ decision = json.loads(outcome.stdout)
67
+ assert decision["hookSpecificOutput"]["permissionDecision"] == "deny"
68
+ assert "asyncio" in decision["hookSpecificOutput"]["permissionDecisionReason"]
69
+
70
+
71
+ def test_blocks_edit_that_deletes_the_line_the_comment_names(tmp_path: Path) -> None:
72
+ """Deleting the patched line under an unchanged comment naming it is
73
+ denied the same as rewriting it would be.
74
+ """
75
+ written_module_path = _write_module(
76
+ tmp_path,
77
+ "def launch_export() -> None:\n" + KEPT_COMMENT_LINE + PATCHED_SLEEP_LINE,
78
+ )
79
+ outcome = _run_hook(
80
+ "Edit",
81
+ {
82
+ "file_path": str(written_module_path),
83
+ "old_string": PATCHED_SLEEP_LINE,
84
+ "new_string": "",
85
+ },
86
+ )
87
+ assert outcome.returncode == 0
88
+ decision = json.loads(outcome.stdout)
89
+ assert decision["hookSpecificOutput"]["permissionDecision"] == "deny"
90
+ assert "asyncio" in decision["hookSpecificOutput"]["permissionDecisionReason"]
91
+
92
+
93
+ def test_allows_edit_that_updates_the_comment_together(tmp_path: Path) -> None:
94
+ """An edit rewriting the comment and the line together passes."""
95
+ written_module_path = _write_module(
96
+ tmp_path,
97
+ "def launch_export() -> None:\n" + KEPT_COMMENT_LINE + PATCHED_SLEEP_LINE,
98
+ )
99
+ outcome = _run_hook(
100
+ "Edit",
101
+ {
102
+ "file_path": str(written_module_path),
103
+ "old_string": KEPT_COMMENT_LINE + PATCHED_SLEEP_LINE,
104
+ "new_string": " # Mock the overlay poll\n" + PATCHED_POLL_LINE,
105
+ },
106
+ )
107
+ assert outcome.returncode == 0
108
+ assert outcome.stdout == ""
109
+
110
+
111
+ def test_allows_edit_that_keeps_the_named_identifier(tmp_path: Path) -> None:
112
+ """A rewrite that still carries the identifier the comment names passes."""
113
+ written_module_path = _write_module(
114
+ tmp_path,
115
+ "def launch_export() -> None:\n" + KEPT_COMMENT_LINE + PATCHED_SLEEP_LINE,
116
+ )
117
+ retained_line = " with patch('theme_exports.core.exporter.asyncio.sleep'):\n"
118
+ outcome = _run_hook(
119
+ "Edit",
120
+ {
121
+ "file_path": str(written_module_path),
122
+ "old_string": PATCHED_SLEEP_LINE.rstrip("\n"),
123
+ "new_string": retained_line.rstrip("\n"),
124
+ },
125
+ )
126
+ assert outcome.returncode == 0
127
+ assert outcome.stdout == ""
128
+
129
+
130
+ def test_allows_prose_comment_with_no_orphaned_identifier(tmp_path: Path) -> None:
131
+ """A prose comment whose tokens never name anything in the edited line
132
+ passes.
133
+ """
134
+ written_module_path = _write_module(
135
+ tmp_path,
136
+ "def launch_export() -> None:\n"
137
+ " # Overlay settle pause\n"
138
+ " runner.pause_for_overlay_settle()\n",
139
+ )
140
+ outcome = _run_hook(
141
+ "Edit",
142
+ {
143
+ "file_path": str(written_module_path),
144
+ "old_string": " runner.pause_for_overlay_settle()",
145
+ "new_string": " runner.confirm_editor_ready()",
146
+ },
147
+ )
148
+ assert outcome.returncode == 0
149
+ assert outcome.stdout == ""
150
+
151
+
152
+ def test_allows_non_python_file(tmp_path: Path) -> None:
153
+ """A non-Python target passes untouched."""
154
+ plain_text_target = tmp_path / "notes.txt"
155
+ plain_text_target.write_text("# Mock asyncio\nasyncio row\n", encoding="utf-8")
156
+ outcome = _run_hook(
157
+ "Edit",
158
+ {
159
+ "file_path": str(plain_text_target),
160
+ "old_string": "asyncio row",
161
+ "new_string": "other row",
162
+ },
163
+ )
164
+ assert outcome.returncode == 0
165
+ assert outcome.stdout == ""
166
+
167
+
168
+ def test_allows_write_tool() -> None:
169
+ """A non-Edit invocation passes untouched."""
170
+ outcome = _run_hook(
171
+ "Write",
172
+ {
173
+ "file_path": "src/sample_module.py",
174
+ "content": KEPT_COMMENT_LINE + PATCHED_POLL_LINE,
175
+ },
176
+ )
177
+ assert outcome.returncode == 0
178
+ assert outcome.stdout == ""
179
+
180
+
181
+ def test_allows_missing_file(tmp_path: Path) -> None:
182
+ """An Edit aimed at a path with no readable target passes."""
183
+ outcome = _run_hook(
184
+ "Edit",
185
+ {
186
+ "file_path": str(tmp_path / "absent_module.py"),
187
+ "old_string": "one",
188
+ "new_string": "two",
189
+ },
190
+ )
191
+ assert outcome.returncode == 0
192
+ assert outcome.stdout == ""
193
+
194
+
195
+ def test_allows_old_string_not_found(tmp_path: Path) -> None:
196
+ """An Edit whose old_string never occurs in the target passes."""
197
+ written_module_path = _write_module(tmp_path, "value_count = 1\n")
198
+ outcome = _run_hook(
199
+ "Edit",
200
+ {
201
+ "file_path": str(written_module_path),
202
+ "old_string": "never present",
203
+ "new_string": "still never present",
204
+ },
205
+ )
206
+ assert outcome.returncode == 0
207
+ assert outcome.stdout == ""
208
+
209
+
210
+ def test_deny_payload_logs_the_block(tmp_path: Path) -> None:
211
+ """One record lands in the block journal for each denial built through
212
+ evaluate plus build_deny_payload.
213
+ """
214
+ written_module_path = _write_module(
215
+ tmp_path,
216
+ "def launch_export() -> None:\n" + KEPT_COMMENT_LINE + PATCHED_SLEEP_LINE,
217
+ )
218
+ deny_reason = evaluate(
219
+ {
220
+ "tool_name": "Edit",
221
+ "tool_input": {
222
+ "file_path": str(written_module_path),
223
+ "old_string": PATCHED_SLEEP_LINE.rstrip("\n"),
224
+ "new_string": PATCHED_POLL_LINE.rstrip("\n"),
225
+ },
226
+ }
227
+ )
228
+ assert deny_reason is not None
229
+ with patch.object(Path, "home", return_value=tmp_path):
230
+ deny_payload = build_deny_payload(deny_reason)
231
+ assert deny_payload["hookSpecificOutput"]["permissionDecision"] == "deny"
232
+ block_log_path = tmp_path / _HOOK_BLOCKS_LOG_RELATIVE_PATH
233
+ all_log_records = block_log_path.read_text(encoding="utf-8").strip().splitlines()
234
+ assert len(all_log_records) == 1
235
+ parsed_log_line = json.loads(all_log_records[0])
236
+ assert parsed_log_line["hook"] == "stale_comment_reference_blocker.py"
@@ -633,6 +633,141 @@ def test_handles_non_string_tool_name():
633
633
  assert result.stdout == ""
634
634
 
635
635
 
636
+ def test_detects_used_to_in_python_module_docstring() -> None:
637
+ """A module docstring carrying a transitional phrase counts as
638
+ documentation and is denied.
639
+ """
640
+ written_source = (
641
+ '"""Both paths used to pad with a flat sleep on every export."""\n'
642
+ "\n"
643
+ "RETRY_BUDGET = 5\n"
644
+ )
645
+ result = _run_hook(
646
+ "Write",
647
+ {
648
+ "file_path": "tests/test_exports.py",
649
+ "content": written_source,
650
+ },
651
+ )
652
+ assert result.returncode == 0
653
+ output = json.loads(result.stdout)
654
+ assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
655
+ assert "used to" in output["hookSpecificOutput"]["permissionDecisionReason"]
656
+
657
+
658
+ def test_detects_previously_in_python_function_docstring() -> None:
659
+ """A function docstring carrying a transitional phrase is denied.
660
+ """
661
+ written_source = (
662
+ "def load_configuration() -> None:\n"
663
+ ' """Previously configured via an environment variable."""\n'
664
+ " raise ValueError\n"
665
+ )
666
+ result = _run_hook(
667
+ "Write",
668
+ {
669
+ "file_path": "src/configuration.py",
670
+ "content": written_source,
671
+ },
672
+ )
673
+ assert result.returncode == 0
674
+ output = json.loads(result.stdout)
675
+ assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
676
+ assert "previously" in output["hookSpecificOutput"]["permissionDecisionReason"]
677
+
678
+
679
+ def test_ignores_transitional_phrase_in_regular_python_string() -> None:
680
+ """A plain string literal counts as data rather than documentation and
681
+ passes.
682
+ """
683
+ result = _run_hook(
684
+ "Write",
685
+ {
686
+ "file_path": "src/messages.py",
687
+ "content": 'PADDING_NOTE = "used to pad with a flat sleep"\n',
688
+ },
689
+ )
690
+ assert result.returncode == 0
691
+ assert result.stdout == ""
692
+
693
+
694
+ def test_ignores_quoted_mention_inside_python_docstring() -> None:
695
+ """A docstring quoting a phrase as a mention in double quotes passes.
696
+ """
697
+ written_source = (
698
+ '"""Blocks docstrings carrying phrases like "used to" in their prose."""\n'
699
+ "\n"
700
+ "RETRY_BUDGET = 3\n"
701
+ )
702
+ result = _run_hook(
703
+ "Write",
704
+ {
705
+ "file_path": "src/scanner.py",
706
+ "content": written_source,
707
+ },
708
+ )
709
+ assert result.returncode == 0
710
+ assert result.stdout == ""
711
+
712
+
713
+ def test_detects_docstring_at_start_of_unparseable_edit_fragment() -> None:
714
+ """An Edit fragment opening with a docstring is scanned even when
715
+ parsing fails.
716
+ """
717
+ fragment = '"""The runner used to sleep between exports."""\n continue\n'
718
+ result = _run_hook(
719
+ "Edit",
720
+ {
721
+ "file_path": "src/exports.py",
722
+ "old_string": "placeholder",
723
+ "new_string": fragment,
724
+ },
725
+ )
726
+ assert result.returncode == 0
727
+ output = json.loads(result.stdout)
728
+ assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
729
+ assert "used to" in output["hookSpecificOutput"]["permissionDecisionReason"]
730
+
731
+
732
+ def test_detects_docstring_after_def_header_in_unparseable_fragment() -> None:
733
+ """A triple-quoted block directly under a def header counts as a
734
+ docstring in a fragment.
735
+ """
736
+ fragment = (
737
+ " def render_summary(self) -> None:\n"
738
+ ' """No longer writes the summary sheet."""\n'
739
+ )
740
+ result = _run_hook(
741
+ "Edit",
742
+ {
743
+ "file_path": "src/reporting.py",
744
+ "old_string": "placeholder",
745
+ "new_string": fragment,
746
+ },
747
+ )
748
+ assert result.returncode == 0
749
+ output = json.loads(result.stdout)
750
+ assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
751
+ assert "no longer" in output["hookSpecificOutput"]["permissionDecisionReason"]
752
+
753
+
754
+ def test_ignores_assigned_triple_quoted_string_in_unparseable_fragment() -> None:
755
+ """A triple-quoted block assigned to a name counts as data rather than
756
+ a docstring in a fragment.
757
+ """
758
+ fragment = 'FIXTURE_TEXT = """used to pad with a flat sleep"""\n continue\n'
759
+ result = _run_hook(
760
+ "Edit",
761
+ {
762
+ "file_path": "src/fixtures.py",
763
+ "old_string": "placeholder",
764
+ "new_string": fragment,
765
+ },
766
+ )
767
+ assert result.returncode == 0
768
+ assert result.stdout == ""
769
+
770
+
636
771
  def test_native_dispatch_path_logs_the_block(tmp_path: Path) -> None:
637
772
  """A deny routed through the dispatcher's native path logs one record.
638
773
 
package/hooks/hooks.json CHANGED
@@ -60,6 +60,11 @@
60
60
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/conventional_pr_title_gate.py",
61
61
  "timeout": 10
62
62
  },
63
+ {
64
+ "type": "command",
65
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/reviewer_spawn_gate.py",
66
+ "timeout": 20
67
+ },
63
68
  {
64
69
  "type": "command",
65
70
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/block_main_commit.py",
@@ -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 |
@@ -53,11 +50,13 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
53
50
  | `precommit_code_rules_gate_constants.py` | Scope argument and exit-code constants for the precommit gate |
54
51
  | `project_paths_reader.py` | Loads `~/.claude/project-paths.json` — the per-user project-path registry |
55
52
  | `pytest_testpaths_orphan_blocker_constants.py` | Marker filename, section and key names, test-file pattern, search budget, and block-message text for the pytest unregistered-test-directory blocker |
53
+ | `reviewer_spawn_gate_constants.py` | Bash tool name, the sentinel marker, the Copilot and Bugbot trigger markers, the availability-script relative path and override env-var name, and the deny-message template for the reviewer-spawn gate |
56
54
  | `send_user_file_open_locally_blocker_constants.py` | Tool name, proactive status, and the block message for the open-locally attach blocker |
57
55
  | `session_env_cleanup_constants.py` | Stale-age threshold and directory names for the session-env cleanup hook |
58
56
  | `session_handoff_blocker_constants.py` | Trigger phrases for the session-handoff blocker |
59
57
  | `setup_project_paths_constants.py` | Encoding policy, BOM marker, and registry meta-key used across multiple hooks |
60
- | `state_description_blocker_constants.py` | The set of historical/comparative phrases the state-description blocker rejects |
58
+ | `stale_comment_reference_blocker_constants.py` | Identifier pattern, comment stopwords, and denial text for the stale-comment-reference blocker |
59
+ | `state_description_blocker_constants.py` | The set of historical/comparative phrases the state-description blocker rejects, plus the docstring-extraction and mention-span patterns for its Python docstring scan |
61
60
  | `stuttering_check_config.py` | Config for the stuttering (repeated-phrase) check |
62
61
  | `stuttering_import_binding_constants.py` | Import-binding patterns for the stuttering check |
63
62
  | `subprocess_budget_completeness_constants.py` | Required argument names for the subprocess-budget completeness check |
@@ -28,7 +28,20 @@ MAX_THIN_WRAPPER_ISSUES: int = 1
28
28
  MAX_ZERO_PAYLOAD_ALIAS_ISSUES: int = 3
29
29
  MAX_LOGGING_FSTRING_ISSUES: int = 3
30
30
  MAX_LOGGING_PRINTF_TOKEN_ISSUES: int = 3
31
+ MAX_LOGGING_ADJACENT_LITERAL_ISSUES: int = 3
32
+ MAX_CONFIG_DUPLICATE_PATH_ANCHOR_ISSUES: int = 3
31
33
  MAX_WINDOWS_API_NONE_ISSUES: int = 3
34
+ MAX_NAIVE_DATETIME_ISSUES: int = 3
35
+ DATETIME_CLASS_ATTRIBUTE_NAME: str = "datetime"
36
+ NAIVE_DATETIME_TIMEZONE_KEYWORD: str = "tz"
37
+ NAIVE_DATETIME_UTCNOW_CONSTRUCTOR: str = "utcnow"
38
+ NAIVE_DATETIME_UTCFROMTIMESTAMP_CONSTRUCTOR: str = "utcfromtimestamp"
39
+ NAIVE_DATETIME_FROMTIMESTAMP_CONSTRUCTOR: str = "fromtimestamp"
40
+ FROMTIMESTAMP_POSITIONAL_TIMEZONE_ARGUMENT_COUNT: int = 2
41
+ ALL_ALWAYS_NAIVE_DATETIME_CONSTRUCTORS: tuple[str, ...] = (
42
+ NAIVE_DATETIME_UTCNOW_CONSTRUCTOR,
43
+ NAIVE_DATETIME_UTCFROMTIMESTAMP_CONSTRUCTOR,
44
+ )
32
45
  MAX_E2E_TEST_NAMING_ISSUES: int = 3
33
46
  MAX_IMPORT_BLOCK_SORT_ISSUES: int = 1
34
47
  IMPORT_BLOCK_SORT_RUFF_TIMEOUT_SECONDS: int = 15
@@ -48,6 +61,11 @@ ALL_IMPORT_BLOCK_SORT_RUFF_COMMAND_PREFIX: tuple[str, ...] = (
48
61
  )
49
62
  MAX_JS_RESUME_TASK_ENUMERATION_ISSUES: int = 5
50
63
  MAX_JS_RETURNS_OBJECT_SCHEMALESS_ISSUES: int = 5
64
+ MAX_JS_SIBLING_RETURN_OBJECT_KEY_DRIFT_ISSUES: int = 5
65
+ MINIMUM_SIBLING_RETURN_OBJECT_KEYS: int = 2
66
+ SIBLING_RETURN_OBJECT_EXACT_MISSING_KEY_COUNT: int = 1
67
+ SIBLING_RETURN_OBJECT_SINGLE_AGREED_MISSING_KEY_COUNT: int = 1
68
+ MAX_JS_BARE_FLAG_RETURN_DIRECTIVE_ISSUES: int = 5
51
69
  MINIMUM_RESUME_TASK_ENUMERATION_ITEMS: int = 2
52
70
  DOCSTRING_TRIVIAL_FUNCTION_BODY_LINE_LIMIT: int = 3
53
71
  MAX_DOCSTRING_FALLBACK_BRANCH_ISSUES: int = 3
@@ -59,6 +77,7 @@ MAX_STALE_TEST_NAME_TARGET_ISSUES: int = 3
59
77
  STALE_TEST_NAME_MINIMUM_SHARED_TOKEN_COUNT: int = 2
60
78
  MAX_MODULE_DOCSTRING_CHECK_ROSTER_ISSUES: int = 5
61
79
  MINIMUM_PUBLIC_CHECKS_FOR_MODULE_DOCSTRING_ROSTER: int = 2
80
+ MINIMUM_SIBLING_OCCURRENCES_FOR_SHARED_TOKEN: int = 2
62
81
  MAX_DOCSTRING_TUPLE_ENUMERATION_ISSUES: int = 5
63
82
  MINIMUM_TUPLE_MEMBERS_FOR_DOCSTRING_ENUMERATION: int = 2
64
83
  MAX_DOCSTRING_MARK_GLYPH_ENUMERATION_ISSUES: int = 5
@@ -199,6 +218,9 @@ DOCSTRING_RUNON_SENTENCE_WORD_LIMIT: int = 30
199
218
  MAX_DOCSTRING_RUNON_SENTENCE_ISSUES: int = 5
200
219
  ALL_DOCSTRING_RUNON_JOINER_MARKERS: tuple[str, ...] = ("—", " -- ", ";")
201
220
  DOCSTRING_RUNON_SENTENCE_BOUNDARY_PATTERN: re.Pattern[str] = re.compile(r"(?<=[.!?])\s+")
221
+ DOCSTRING_NARRATIVE_PROSE_LINE_LIMIT: int = 6
222
+ DOCSTRING_NARRATIVE_LINE_JOIN_SEPARATOR: str = " "
223
+ MAX_DOCSTRING_PROSE_WALL_ISSUES: int = 5
202
224
 
203
225
  ALL_DOCSTRING_NO_CONSUMER_CLAIM_PHRASES: tuple[str, ...] = (
204
226
  "no consumer reads",
@@ -290,6 +312,30 @@ ALL_TEST_INDICATING_ENVIRONMENT_VARIABLE_NAMES: frozenset[str] = frozenset(
290
312
  }
291
313
  )
292
314
 
315
+ MAX_DOCSTRING_DELEGATION_ENUMERATION_ISSUES: int = 5
316
+ MINIMUM_DELEGATION_ENUMERATION_ITEMS: int = 2
317
+ NEIGHBOR_SCAN_FILE_LIMIT: int = 40
318
+ POINTER_TO_DELEGATE_PATTERN: re.Pattern[str] = re.compile(
319
+ r"full doc(?:umentation)?\s+on\s+`{1,2}([A-Za-z_][A-Za-z0-9_]*)`{1,2}",
320
+ re.IGNORECASE,
321
+ )
322
+ TOKEN_WORD_PATTERN: re.Pattern[str] = re.compile(r"[A-Za-z][A-Za-z0-9]{2,}")
323
+ ALL_DELEGATION_ENUMERATION_STOP_TOKENS: frozenset[str] = frozenset(
324
+ {
325
+ "the",
326
+ "and",
327
+ "then",
328
+ "edit",
329
+ "edits",
330
+ "apply",
331
+ "applies",
332
+ "run",
333
+ "runs",
334
+ "step",
335
+ "steps",
336
+ }
337
+ )
338
+
293
339
  MAX_MODULE_DOCSTRING_DATA_SCHEMA_SCOPE_ISSUES: int = 1
294
340
  MODULE_DOCSTRING_DATA_SCHEMA_CONSTANT_SAMPLE_LIMIT: int = 6
295
341
  ALL_USER_FACING_TEXT_SCOPE_DOCSTRING_PHRASES: tuple[str, ...] = (
@@ -185,7 +185,18 @@ LOGGING_FSTRING_PATTERN = re.compile(
185
185
  LOGGING_PRINTF_TOKEN_PATTERN: re.Pattern[str] = re.compile(
186
186
  r"(?<!%)%[#0\- +]?[0-9.*]*[sdrixfgeEcoX](?![a-zA-Z])"
187
187
  )
188
+ ADJACENT_STRING_LITERAL_PATTERN: re.Pattern[str] = re.compile(
189
+ r'(?<!["\'])[rRbBfFuU]{0,2}(?:"(?:[^"\\\n]|\\.)*"|\'(?:[^\'\\\n]|\\.)*\')'
190
+ r'\s*[rRbBfFuU]{0,2}(?:"(?:[^"\\\n]|\\.)*"|\'(?:[^\'\\\n]|\\.)*\')(?!["\'])'
191
+ )
188
192
  MINIMUM_FORMAT_LOGGER_ARGUMENT_COUNT = 2
193
+ ALL_LOGGING_CALL_METHOD_NAMES: frozenset[str] = frozenset(
194
+ {"debug", "info", "warning", "error", "critical", "exception"}
195
+ )
196
+ LOGGING_HELPER_FUNCTION_NAME_PATTERN: re.Pattern[str] = re.compile(
197
+ r"^log_(?:debug|info|warning|error|critical|exception)$"
198
+ )
199
+ LOGGING_RECEIVER_NAME_PATTERN: re.Pattern[str] = re.compile(r"^_?(?:logger|logging|log)$")
189
200
  SPAWN_AGENT_WITH_JSDOC_PATTERN: re.Pattern[str] = re.compile(
190
201
  r"/\*\*(?P<jsdoc>(?:(?!\*/).)*?)\*/\s*"
191
202
  r"(?:async\s+)?function\s+spawn(?P<role>\w+?)Agent\s*\(",
@@ -233,9 +244,39 @@ JSDOC_RETURNS_STRUCTURED_OBJECT_PROMISE_PATTERN: re.Pattern[str] = re.compile(
233
244
  RETURN_CALL_OPENING_PARENTHESIS_PATTERN: re.Pattern[str] = re.compile(
234
245
  r"\breturn\s+(?:await\s+)?(?P<callee>\w+)\s*\("
235
246
  )
247
+ BARE_FLAG_CONTRACT_PATTERN: re.Pattern[str] = re.compile(
248
+ r"never a bare (?P<flag>\w+) flag", re.IGNORECASE
249
+ )
250
+ BARE_FLAG_RETURN_DIRECTIVE_PATTERN: re.Pattern[str] = re.compile(
251
+ r"\breturn\s+(?P<flag>\w+)\s*:\s*(?:true|false)\b", re.IGNORECASE
252
+ )
236
253
  SCHEMA_OPTIONS_PROPERTY_KEY_PATTERN: re.Pattern[str] = re.compile(
237
254
  r"(?<![A-Za-z0-9_])schema\s*:"
238
255
  )
256
+ RETURN_OBJECT_LITERAL_OPENING_PATTERN: re.Pattern[str] = re.compile(r"\breturn\s*\{")
257
+ ARROW_CONCISE_BODY_OBJECT_LITERAL_OPENING_PATTERN: re.Pattern[str] = re.compile(
258
+ r"=>\s*\(\s*\{"
259
+ )
260
+ JS_OBJECT_KEY_IDENTIFIER_PATTERN: re.Pattern[str] = re.compile(r"[A-Za-z_$][\w$]*")
261
+ JS_OBJECT_METHOD_SHORTHAND_KEY_PATTERN: re.Pattern[str] = re.compile(
262
+ r"\A([A-Za-z_$][\w$]*)\s*\("
263
+ )
264
+ ALL_JAVASCRIPT_CONTROL_FLOW_BLOCK_KEYWORDS: frozenset[str] = frozenset(
265
+ {"if", "for", "await", "while", "switch", "catch", "with"}
266
+ )
267
+ ALL_JAVASCRIPT_PARENTHESIS_FREE_BLOCK_KEYWORDS: frozenset[str] = frozenset(
268
+ {"else", "do", "try", "finally"}
269
+ )
270
+ ALL_JAVASCRIPT_BRACKET_OPENERS: frozenset[str] = frozenset({"(", "[", "{"})
271
+ ALL_JAVASCRIPT_BRACKET_CLOSERS: frozenset[str] = frozenset({")", "]", "}"})
272
+ ALL_JAVASCRIPT_IDENTIFIER_EXTRA_CHARACTERS: frozenset[str] = frozenset({"_", "$"})
273
+ ALL_JAVASCRIPT_RETURN_TYPE_TERMINATORS: frozenset[str] = frozenset({"}", ">", "]"})
274
+ JAVASCRIPT_ARROW_TOKEN_LENGTH: int = 2
275
+ ALL_JAVASCRIPT_BARE_RETURN_TYPE_EXTRA_CHARACTERS: frozenset[str] = frozenset(
276
+ {"_", "$", ".", "|", "&", "?"}
277
+ )
278
+ JAVASCRIPT_OBJECT_SPREAD_PREFIX: str = "..."
279
+ JAVASCRIPT_MODULE_SCOPE_SENTINEL: int = -1
239
280
  ALL_BUILTIN_DICT_METHOD_NAMES: frozenset[str] = frozenset({
240
281
  "get", "items", "keys", "values", "update", "pop",
241
282
  "setdefault", "copy", "clear",
@@ -22,8 +22,8 @@ ALL_CONFIG_DIRECTORY_NAMES = frozenset(
22
22
  "pr_converge_skill_constants",
23
23
  "pr_converge_scripts_constants",
24
24
  "bugteam_scripts_constants",
25
- "doc_gist_scripts_constants",
26
25
  "implement_scripts_constants",
26
+ "log_audit_constants",
27
27
  "dev_env_scripts_constants",
28
28
  }
29
29
  )
@@ -19,7 +19,6 @@ ALL_EXEMPT_PATH_FRAGMENTS: tuple[str, ...] = (
19
19
  "windows_safe_rmtree.py",
20
20
  "windows_filesystem.py",
21
21
  "session_env_cleanup.py",
22
- "_md_to_html_blocker_test_support.py",
23
22
  "teardown_worktrees.py",
24
23
  )
25
24
 
@@ -62,9 +62,4 @@ ALL_POST_HOSTED_HOOK_ENTRIES: tuple[PostHostedHookEntry, ...] = (
62
62
  script_relative_path="workflow/auto_formatter.py",
63
63
  is_blocking=False,
64
64
  ),
65
- PostHostedHookEntry(
66
- script_relative_path="workflow/doc_gist_auto_publish.py",
67
- extra_argument_relative_paths=(PLUGIN_ROOT_PLACEHOLDER,),
68
- is_blocking=False,
69
- ),
70
65
  )
@@ -105,6 +105,10 @@ ALL_HOSTED_HOOK_ENTRIES: tuple[HostedHookEntry, ...] = (
105
105
  applicable_tool_names=ALL_WRITE_AND_EDIT_TOOL_NAMES,
106
106
  native_module_name=STATE_DESCRIPTION_BLOCKER_MODULE_NAME,
107
107
  ),
108
+ HostedHookEntry(
109
+ script_relative_path="blocking/stale_comment_reference_blocker.py",
110
+ applicable_tool_names=frozenset({EDIT_TOOL_NAME}),
111
+ ),
108
112
  HostedHookEntry(
109
113
  script_relative_path="blocking/subprocess_budget_completeness.py",
110
114
  applicable_tool_names=ALL_WRITE_AND_EDIT_TOOL_NAMES,
@@ -0,0 +1,41 @@
1
+ """Configuration constants for the reviewer_spawn_gate PreToolUse hook."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+
8
+ BASH_TOOL_NAME: str = "Bash"
9
+
10
+ GATE_SENTINEL_MARKER: str = "CLAUDE_REVIEWER_GATE=autoconverge"
11
+
12
+ COPILOT_REVIEWER_TOKEN: str = "copilot"
13
+ BUGBOT_REVIEWER_TOKEN: str = "bugbot"
14
+
15
+ COPILOT_REVIEWER_LABEL: str = "GitHub Copilot"
16
+ BUGBOT_REVIEWER_LABEL: str = "Cursor Bugbot"
17
+
18
+ ALL_COPILOT_TRIGGER_MARKERS: tuple[str, ...] = (
19
+ "requested_reviewers",
20
+ "copilot-pull-request-reviewer[bot]",
21
+ )
22
+
23
+ BUGBOT_TRIGGER_SCRIPT_MARKER: str = "post_fix_reply.py"
24
+ BUGBOT_RUN_BODY_PATTERN: re.Pattern[str] = re.compile(
25
+ r'--body\s+["\']bugbot run["\']', re.IGNORECASE
26
+ )
27
+
28
+ AVAILABILITY_SCRIPT_RELATIVE_PATH: Path = (
29
+ Path("_shared") / "pr-loop" / "scripts" / "reviewer_availability.py"
30
+ )
31
+ AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME: str = "REVIEWER_SPAWN_GATE_AVAILABILITY_SCRIPT_PATH"
32
+ AVAILABILITY_SCRIPT_TIMEOUT_SECONDS: int = 15
33
+ AVAILABILITY_REVIEWER_FLAG: str = "--reviewer"
34
+ AVAILABILITY_DOWN_EXIT_CODE: int = 3
35
+
36
+ DENY_REASON_TEMPLATE: str = (
37
+ "BLOCKED [reviewer-spawn-gate]: {reviewer_label} is down or out of quota "
38
+ "this run. Skip requesting {reviewer_label} on this trigger — "
39
+ "autoconverge requests it again once the availability check reports it "
40
+ "back.\n\nAvailability check output: {availability_detail}"
41
+ )