claude-dev-env 2.4.0 → 2.5.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 (195) hide show
  1. package/CLAUDE.md +53 -49
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
  4. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
  6. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
  8. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
  9. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
  10. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
  12. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
  13. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
  14. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
  15. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
  16. package/_shared/pr-loop/worker-spawn.md +1 -1
  17. package/agents/CLAUDE.md +2 -1
  18. package/agents/caveman.md +0 -1
  19. package/agents/clasp-deployment-orchestrator.md +0 -1
  20. package/agents/clean-coder.md +0 -1
  21. package/agents/code-advisor.md +0 -1
  22. package/agents/code-quality-agent.md +1 -2
  23. package/agents/code-verifier.md +0 -1
  24. package/agents/deep-research.md +0 -1
  25. package/agents/docs-agent.md +0 -1
  26. package/agents/git-commit-crafter.md +0 -1
  27. package/agents/issue-tracker.md +42 -0
  28. package/agents/plan-packet-validator.md +0 -1
  29. package/agents/pr-description-writer.md +0 -1
  30. package/agents/test_agent_frontmatter.py +67 -18
  31. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
  32. package/bin/CLAUDE.md +68 -5
  33. package/bin/ever-shipped-skills.mjs +1 -0
  34. package/bin/install-constants.mjs +88 -0
  35. package/bin/install.mjs +1138 -114
  36. package/bin/install.prune.test.mjs +869 -19
  37. package/bin/install.test.mjs +906 -2
  38. package/commands/implement.md +1 -1
  39. package/commands/right-size.md +1 -1
  40. package/docs/CLAUDE.md +1 -0
  41. package/docs/host-pool-health-monitor.md +102 -0
  42. package/docs/references/CLAUDE.md +4 -2
  43. package/docs/references/advisor-tool.md +13 -0
  44. package/docs/references/code-review-enforcement.md +10 -0
  45. package/docs/references/team-advisor-skill.md +14 -0
  46. package/hooks/blocking/CLAUDE.md +1 -0
  47. package/hooks/blocking/code_review_pr_create_gate.py +7 -3
  48. package/hooks/blocking/code_review_push_gate.py +9 -4
  49. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
  50. package/hooks/blocking/config/__init__.py +5 -5
  51. package/hooks/blocking/config/code_review_enforcement_constants.py +4 -1
  52. package/hooks/blocking/config/test_code_review_enforcement_constants.py +5 -0
  53. package/hooks/blocking/config/verified_commit_constants.py +160 -159
  54. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
  55. package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
  56. package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
  57. package/hooks/blocking/test_code_review_push_gate.py +16 -0
  58. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
  59. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
  60. package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
  61. package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
  62. package/hooks/blocking/test_verification_verdict_store.py +54 -0
  63. package/hooks/blocking/test_verified_commit_gate.py +581 -581
  64. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
  65. package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
  66. package/hooks/blocking/verification_verdict_store.py +1039 -1036
  67. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
  68. package/hooks/blocking/verifier_verdict_minter.py +280 -280
  69. package/hooks/git-hooks/test_pre_push.py +25 -0
  70. package/hooks/hooks.json +10 -0
  71. package/hooks/hooks_constants/CLAUDE.md +2 -1
  72. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
  73. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
  74. package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
  75. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
  76. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
  77. package/hooks/pyproject.toml +1 -0
  78. package/hooks/validators/CLAUDE.md +1 -0
  79. package/hooks/validators/config/__init__.py +0 -0
  80. package/hooks/validators/config/directory_exemption_constants.py +183 -0
  81. package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
  82. package/hooks/validators/conftest.py +4 -0
  83. package/hooks/validators/ruff_integration.py +49 -5
  84. package/hooks/validators/run_all_validators.py +206 -9
  85. package/hooks/validators/test_directory_exemption_constants.py +185 -0
  86. package/hooks/validators/test_python_antipattern_checks.py +110 -5
  87. package/hooks/validators/test_ruff_integration.py +92 -1
  88. package/hooks/validators/test_run_all_validators.py +115 -68
  89. package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
  90. package/package.json +10 -2
  91. package/rules/CLAUDE.md +1 -0
  92. package/rules/docstring-prose-matches-implementation.md +45 -44
  93. package/rules/state-what-is.md +25 -0
  94. package/rules/verified-commit-gate-skip.md +1 -1
  95. package/scripts/CLAUDE.md +1 -0
  96. package/scripts/Capture-PoolHealth.ps1 +410 -0
  97. package/scripts/_code_review_test_support.py +404 -0
  98. package/scripts/claude_chain_runner.py +141 -1
  99. package/scripts/conftest.py +16 -1
  100. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
  101. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
  102. package/scripts/resolve_worker_spawn.py +626 -626
  103. package/scripts/spawn_grok_batch.py +672 -672
  104. package/scripts/test_claude_chain_runner.py +131 -0
  105. package/scripts/test_invoke_code_review_chain.py +70 -0
  106. package/scripts/test_invoke_code_review_cli.py +192 -0
  107. package/scripts/test_invoke_code_review_contract.py +256 -0
  108. package/scripts/test_invoke_code_review_git.py +123 -0
  109. package/scripts/test_invoke_code_review_mode.py +99 -0
  110. package/scripts/test_resolve_worker_spawn.py +1014 -1014
  111. package/skills/CLAUDE.md +2 -0
  112. package/skills/auditing-claude-config/SKILL.md +114 -114
  113. package/skills/autoconverge/SKILL.md +427 -427
  114. package/skills/autoconverge/reference/convergence.md +24 -3
  115. package/skills/autoconverge/workflow/CLAUDE.md +1 -0
  116. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
  117. package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
  118. package/skills/autoconverge/workflow/converge.mjs +167 -0
  119. package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
  120. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
  121. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
  122. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
  123. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
  124. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
  125. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
  126. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
  127. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
  128. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
  129. package/skills/closeout/SKILL.md +33 -50
  130. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
  131. package/skills/codex-review/scripts/run_codex_review.py +233 -1
  132. package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
  133. package/skills/condensing-instructions/SKILL.md +81 -0
  134. package/skills/copilot-review/SKILL.md +119 -119
  135. package/skills/e-code-review/SKILL.md +52 -0
  136. package/skills/e-code-review/reference/fix.md +54 -0
  137. package/skills/e-code-review/reference/loop.md +43 -0
  138. package/skills/e-code-review/reference/low.md +57 -0
  139. package/skills/e-code-review/reference/medium.md +153 -0
  140. package/skills/e-code-review/reference/xhigh.md +182 -0
  141. package/skills/e-simplify/SKILL.md +97 -0
  142. package/skills/issue-tracker/SKILL.md +92 -0
  143. package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
  144. package/skills/issue-tracker/reference/handoff-schema.md +64 -0
  145. package/skills/issue-tracker/reference/operation-matrix.md +41 -0
  146. package/skills/orchestrator/SKILL.md +162 -21
  147. package/skills/orchestrator/scripts/status_gate.py +625 -0
  148. package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
  149. package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
  150. package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
  151. package/skills/orchestrator/scripts/test_status_gate.py +439 -0
  152. package/skills/orchestrator-refresh/SKILL.md +110 -35
  153. package/skills/plan-to-pr/SKILL.md +155 -0
  154. package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
  155. package/skills/plan-to-pr/reference/model-routing.md +36 -0
  156. package/skills/plan-to-pr/reference/packet-contract.md +43 -0
  157. package/skills/plan-to-pr/reference/packet-schema.json +57 -0
  158. package/skills/plan-to-pr/reference/process-inventory.md +22 -0
  159. package/skills/plan-to-pr/reference/review-loop.md +33 -0
  160. package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
  161. package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
  162. package/skills/plan-to-pr/reference/task-seeds.md +14 -0
  163. package/skills/plan-to-pr/reference/task-ticket.md +38 -0
  164. package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
  165. package/skills/plan-to-pr/scripts/config/constants.py +193 -0
  166. package/skills/plan-to-pr/scripts/create_packet.py +173 -0
  167. package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
  168. package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
  169. package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
  170. package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
  171. package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
  172. package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
  173. package/skills/plan-to-pr/scripts/validate_run.py +173 -0
  174. package/skills/plan-to-pr/test_skill_contract.py +207 -0
  175. package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
  176. package/skills/pr-converge/SKILL.md +472 -469
  177. package/skills/pr-converge/reference/examples.md +3 -3
  178. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  179. package/skills/pr-converge/reference/ground-rules.md +7 -4
  180. package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
  181. package/skills/pr-converge/reference/per-tick.md +5 -5
  182. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  183. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
  184. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
  185. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
  186. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  187. package/skills/pr-fix-protocol/SKILL.md +1 -1
  188. package/skills/privacy-hygiene/SKILL.md +68 -68
  189. package/skills/prototype/workflows/promotion.md +1 -1
  190. package/skills/release-notes-html/SKILL.md +164 -0
  191. package/skills/task-build/CLAUDE.md +8 -7
  192. package/skills/task-build/SKILL.md +16 -8
  193. package/skills/task-build/reference/tool-routing.md +19 -0
  194. package/scripts/test_invoke_code_review.py +0 -966
  195. package/skills/closeout/reference/issue-body-templates.md +0 -108
@@ -1,167 +1,167 @@
1
- #!/usr/bin/env python3
2
- """PreToolUse hook: keep verified-commit exemption messages accurate.
3
-
4
- The verified-commit gate exempts a branch surface only when every changed path
5
- matches one of two narrow rules, implemented in ``is_verification_exempt_diff``:
6
-
7
- 1. file extension in ``DOCS_ONLY_EXTENSIONS`` (docs and image extensions), or
8
- 2. a ``.py`` file whose docstring/comment-stripped AST is unchanged.
9
-
10
- A comment-only change to a non-Python, non-doc file (for example ``.sh``,
11
- ``.json``, ``.yaml``) is therefore NOT exempt: comments are ignored for
12
- exemption purposes only inside Python files via the AST path. A corrective or
13
- guard message that claims comment-only or docs-only surfaces are blanket exempt
14
- overstates the rule and misleads users into expecting such a change to skip
15
- verification.
16
-
17
- This hook fires on Write/Edit of any verified-commit constants module and denies
18
- content whose exemption-claim wording asserts a blanket comment-only or docs-only
19
- exemption. It guards the message constants at authoring time, before the change
20
- reaches review.
21
- """
22
-
23
- import json
24
- import os
25
- import re
26
- import sys
27
- from pathlib import Path
28
-
29
- _hooks_dir = str(Path(__file__).resolve().parent.parent)
30
- if _hooks_dir not in sys.path:
31
- sys.path.insert(0, _hooks_dir)
32
-
33
- from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
34
-
35
-
36
- def is_guarded_file(file_path: str) -> bool:
37
- """Return True for any verified-commit constants module carrying messages."""
38
- all_guarded_file_names = frozenset({"verified_commit_constants.py"})
39
- return os.path.basename(file_path) in all_guarded_file_names
40
-
41
-
42
- def join_adjacent_string_literals(written_text: str) -> str:
43
- """Collapse a closing quote, inter-literal whitespace, and an opening quote.
44
-
45
- Write/Edit content carries the message constant as it appears in source: a
46
- long sentence split across adjacent Python string literals. Between two
47
- literals sit a closing quote, a newline, indentation, and an opening quote.
48
- Replacing that run with a single space rejoins the prose so a phrase wrapped
49
- across a literal boundary — for example ``exempt "`` then ``"automatically``
50
- — reads as one continuous clause for matching.
51
- """
52
- inter_literal_noise_pattern = re.compile(r"[\"']\s*[\"']")
53
- return inter_literal_noise_pattern.sub(" ", written_text)
54
-
55
-
56
- def claims_blanket_comment_exemption(written_text: str) -> bool:
57
- """Return True when the text claims comment surfaces are blanket-exempt.
58
-
59
- Rejoins source-wrapped string literals first, then matches only a genuine
60
- blanket form in which a comment surface is the grammatical subject of
61
- "exempt automatically". A comment surface is the bare noun ``comments`` or
62
- the ``comment-only`` category — never the ``comment`` stem inside an
63
- unrelated word such as ``commentary`` or the ``comment-stripped`` AST
64
- qualifier.
65
-
66
- Two blanket shapes match. The direct shape names a comment surface as the
67
- immediate subject of "(are|is) exempt automatically" across a bridge that
68
- crosses no period, semicolon, or comma, so an intervening predicate (for
69
- example "Comments are handled, and docs are exempt automatically") leaves
70
- docs, not comments, as the exemption subject and does not match. The
71
- enumerated shape names a comment item inside an "...-only surfaces are
72
- exempt automatically" list (for example "Docs-, comment-, and test-only
73
- surfaces are exempt automatically").
74
-
75
- An accurate sentence whose true exemption subject is docs, or that qualifies
76
- comments as the stripped input to the AST comparison, does not match. This
77
- isolates the overstated form the verified-commit gate does not honor for
78
- non-Python files.
79
- """
80
- comment_surface = r"comments?-only|comments?\b(?!-)"
81
- blanket_direct_claim = (
82
- "(?:" + comment_surface + r")(?:[^.;,]*?\b)?(?:are|is)\s+exempt\s+automatically"
83
- )
84
- blanket_enumerated_claim = (
85
- "(?:" + comment_surface + "|comment-)"
86
- r"[^.;]*?-only\s+surfaces\s+are\s+exempt\s+automatically"
87
- )
88
- blanket_exemption_claim_pattern = re.compile(
89
- "(?:" + blanket_direct_claim + ")|(?:" + blanket_enumerated_claim + ")",
90
- re.IGNORECASE,
91
- )
92
- joined_text = join_adjacent_string_literals(written_text)
93
- return bool(blanket_exemption_claim_pattern.search(joined_text))
94
-
95
-
96
- def build_corrective_message() -> str:
97
- """Return the deny reason explaining the real, narrower exemption rules."""
98
- accurate_exemption_phrasing = (
99
- "Docs and images are exempt by extension, and Python files whose "
100
- "docstring- and comment-stripped AST is unchanged; a comment-only "
101
- "change to a non-Python file still needs a verdict."
102
- )
103
- return (
104
- "BLOCKED [verified-commit-message-accuracy]: this exemption message "
105
- "claims comment-only surfaces are exempt automatically, but the "
106
- "verified-commit gate exempts comments only inside Python files (via the "
107
- "docstring/comment stripped AST path). A comment-only change to a "
108
- "non-Python file is NOT exempt and still needs a verifier verdict, so "
109
- "the blanket wording misleads users.\n\nDescribe the real exemption "
110
- "rules instead, for example:\n " + accurate_exemption_phrasing
111
- )
112
-
113
-
114
- def extract_written_text(all_written_fields: dict[str, str]) -> str:
115
- """Return the Write ``content`` and Edit ``new_string`` joined for scanning."""
116
- return (
117
- all_written_fields.get("content", "")
118
- + "\n"
119
- + all_written_fields.get("new_string", "")
120
- )
121
-
122
-
123
- def main() -> None:
124
- all_write_edit_tools = frozenset({"Write", "Edit"})
125
- try:
126
- hook_input = json.load(sys.stdin)
127
- except json.JSONDecodeError:
128
- sys.exit(0)
129
-
130
- tool_name = hook_input.get("tool_name", "")
131
- if tool_name not in all_write_edit_tools:
132
- sys.exit(0)
133
-
134
- tool_input = hook_input.get("tool_input", {})
135
- file_path = tool_input.get("file_path", "")
136
- if not file_path:
137
- sys.exit(0)
138
-
139
- if not is_guarded_file(file_path):
140
- sys.exit(0)
141
-
142
- written_text = extract_written_text(tool_input)
143
- if not claims_blanket_comment_exemption(written_text):
144
- sys.exit(0)
145
-
146
- corrective_message = build_corrective_message()
147
- deny_response = {
148
- "hookSpecificOutput": {
149
- "hookEventName": "PreToolUse",
150
- "permissionDecision": "deny",
151
- "permissionDecisionReason": corrective_message,
152
- }
153
- }
154
- log_hook_block(
155
- calling_hook_name="verified_commit_message_accuracy_blocker.py",
156
- hook_event="PreToolUse",
157
- block_reason=corrective_message,
158
- tool_name=tool_name,
159
- offending_input_preview=file_path,
160
- )
161
- print(json.dumps(deny_response))
162
- sys.stdout.flush()
163
- sys.exit(0)
164
-
165
-
166
- if __name__ == "__main__":
167
- main()
1
+ #!/usr/bin/env python3
2
+ """PreToolUse hook: keep verified-commit exemption messages accurate.
3
+
4
+ The verified-commit gate exempts a branch surface only when every changed path
5
+ matches one of two narrow rules, implemented in ``is_verification_exempt_diff``:
6
+
7
+ 1. file extension in ``DOCS_ONLY_EXTENSIONS`` (docs and image extensions), or
8
+ 2. a ``.py`` file whose docstring/comment-stripped AST is unchanged.
9
+
10
+ A comment-only change to a non-Python, non-doc file (for example ``.sh``,
11
+ ``.json``, ``.yaml``) is therefore NOT exempt: comments are ignored for
12
+ exemption purposes only inside Python files via the AST path. A corrective or
13
+ guard message that claims comment-only or docs-only surfaces are blanket exempt
14
+ overstates the rule and misleads users into expecting such a change to skip
15
+ verification.
16
+
17
+ This hook fires on Write/Edit of any verified-commit constants module and denies
18
+ content whose exemption-claim wording asserts a blanket comment-only or docs-only
19
+ exemption. It guards the message constants at authoring time, before the change
20
+ reaches review.
21
+ """
22
+
23
+ import json
24
+ import os
25
+ import re
26
+ import sys
27
+ from pathlib import Path
28
+
29
+ _hooks_dir = str(Path(__file__).resolve().parent.parent)
30
+ if _hooks_dir not in sys.path:
31
+ sys.path.insert(0, _hooks_dir)
32
+
33
+ from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
34
+
35
+
36
+ def is_guarded_file(file_path: str) -> bool:
37
+ """Return True for any verified-commit constants module carrying messages."""
38
+ all_guarded_file_names = frozenset({"verified_commit_constants.py"})
39
+ return os.path.basename(file_path) in all_guarded_file_names
40
+
41
+
42
+ def join_adjacent_string_literals(written_text: str) -> str:
43
+ """Collapse a closing quote, inter-literal whitespace, and an opening quote.
44
+
45
+ Write/Edit content carries the message constant as it appears in source: a
46
+ long sentence split across adjacent Python string literals. Between two
47
+ literals sit a closing quote, a newline, indentation, and an opening quote.
48
+ Replacing that run with a single space rejoins the prose so a phrase wrapped
49
+ across a literal boundary — for example ``exempt "`` then ``"automatically``
50
+ — reads as one continuous clause for matching.
51
+ """
52
+ inter_literal_noise_pattern = re.compile(r"[\"']\s*[\"']")
53
+ return inter_literal_noise_pattern.sub(" ", written_text)
54
+
55
+
56
+ def claims_blanket_comment_exemption(written_text: str) -> bool:
57
+ """Return True when the text claims comment surfaces are blanket-exempt.
58
+
59
+ Rejoins source-wrapped string literals first, then matches only a genuine
60
+ blanket form in which a comment surface is the grammatical subject of
61
+ "exempt automatically". A comment surface is the bare noun ``comments`` or
62
+ the ``comment-only`` category — never the ``comment`` stem inside an
63
+ unrelated word such as ``commentary`` or the ``comment-stripped`` AST
64
+ qualifier.
65
+
66
+ Two blanket shapes match. The direct shape names a comment surface as the
67
+ immediate subject of "(are|is) exempt automatically" across a bridge that
68
+ crosses no period, semicolon, or comma, so an intervening predicate (for
69
+ example "Comments are handled, and docs are exempt automatically") leaves
70
+ docs, not comments, as the exemption subject and does not match. The
71
+ enumerated shape names a comment item inside an "...-only surfaces are
72
+ exempt automatically" list (for example "Docs-, comment-, and test-only
73
+ surfaces are exempt automatically").
74
+
75
+ An accurate sentence whose true exemption subject is docs, or that qualifies
76
+ comments as the stripped input to the AST comparison, does not match. This
77
+ isolates the overstated form the verified-commit gate does not honor for
78
+ non-Python files.
79
+ """
80
+ comment_surface = r"comments?-only|comments?\b(?!-)"
81
+ blanket_direct_claim = (
82
+ "(?:" + comment_surface + r")(?:[^.;,]*?\b)?(?:are|is)\s+exempt\s+automatically"
83
+ )
84
+ blanket_enumerated_claim = (
85
+ "(?:" + comment_surface + "|comment-)"
86
+ r"[^.;]*?-only\s+surfaces\s+are\s+exempt\s+automatically"
87
+ )
88
+ blanket_exemption_claim_pattern = re.compile(
89
+ "(?:" + blanket_direct_claim + ")|(?:" + blanket_enumerated_claim + ")",
90
+ re.IGNORECASE,
91
+ )
92
+ joined_text = join_adjacent_string_literals(written_text)
93
+ return bool(blanket_exemption_claim_pattern.search(joined_text))
94
+
95
+
96
+ def build_corrective_message() -> str:
97
+ """Return the deny reason explaining the real, narrower exemption rules."""
98
+ accurate_exemption_phrasing = (
99
+ "Docs and images are exempt by extension, and Python files whose "
100
+ "docstring- and comment-stripped AST is unchanged; a comment-only "
101
+ "change to a non-Python file still needs a verdict."
102
+ )
103
+ return (
104
+ "BLOCKED [verified-commit-message-accuracy]: this exemption message "
105
+ "claims comment-only surfaces are exempt automatically, but the "
106
+ "verified-commit gate exempts comments only inside Python files (via the "
107
+ "docstring/comment stripped AST path). A comment-only change to a "
108
+ "non-Python file is NOT exempt and still needs a verifier verdict, so "
109
+ "the blanket wording misleads users.\n\nDescribe the real exemption "
110
+ "rules instead, for example:\n " + accurate_exemption_phrasing
111
+ )
112
+
113
+
114
+ def extract_written_text(all_written_fields: dict[str, str]) -> str:
115
+ """Return the Write ``content`` and Edit ``new_string`` joined for scanning."""
116
+ return (
117
+ all_written_fields.get("content", "")
118
+ + "\n"
119
+ + all_written_fields.get("new_string", "")
120
+ )
121
+
122
+
123
+ def main() -> None:
124
+ all_write_edit_tools = frozenset({"Write", "Edit"})
125
+ try:
126
+ hook_input = json.load(sys.stdin)
127
+ except json.JSONDecodeError:
128
+ sys.exit(0)
129
+
130
+ tool_name = hook_input.get("tool_name", "")
131
+ if tool_name not in all_write_edit_tools:
132
+ sys.exit(0)
133
+
134
+ tool_input = hook_input.get("tool_input", {})
135
+ file_path = tool_input.get("file_path", "")
136
+ if not file_path:
137
+ sys.exit(0)
138
+
139
+ if not is_guarded_file(file_path):
140
+ sys.exit(0)
141
+
142
+ written_text = extract_written_text(tool_input)
143
+ if not claims_blanket_comment_exemption(written_text):
144
+ sys.exit(0)
145
+
146
+ corrective_message = build_corrective_message()
147
+ deny_response = {
148
+ "hookSpecificOutput": {
149
+ "hookEventName": "PreToolUse",
150
+ "permissionDecision": "deny",
151
+ "permissionDecisionReason": corrective_message,
152
+ }
153
+ }
154
+ log_hook_block(
155
+ calling_hook_name="verified_commit_message_accuracy_blocker.py",
156
+ hook_event="PreToolUse",
157
+ block_reason=corrective_message,
158
+ tool_name=tool_name,
159
+ offending_input_preview=file_path,
160
+ )
161
+ print(json.dumps(deny_response))
162
+ sys.stdout.flush()
163
+ sys.exit(0)
164
+
165
+
166
+ if __name__ == "__main__":
167
+ main()