claude-dev-env 2.9.0 → 2.10.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 (289) hide show
  1. package/CLAUDE.md +1 -1
  2. package/_shared/pr-loop/CLAUDE.md +1 -1
  3. package/_shared/pr-loop/audit-contract.md +17 -6
  4. package/_shared/pr-loop/audit-reply-template.md +4 -4
  5. package/_shared/pr-loop/code-rules-gate.md +3 -5
  6. package/_shared/pr-loop/fix-protocol.md +2 -3
  7. package/_shared/pr-loop/gh-payloads.md +1 -1
  8. package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
  9. package/_shared/pr-loop/scripts/README.md +1 -1
  10. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +16 -1
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +11 -5
  13. package/_shared/pr-loop/scripts/preflight.py +9 -4
  14. package/_shared/pr-loop/scripts/reviews_disabled.py +50 -22
  15. package/_shared/pr-loop/scripts/tests/conftest.py +20 -0
  16. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +6 -6
  17. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +50 -6
  18. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
  19. package/_shared/pr-loop/state-schema.md +5 -14
  20. package/agents/CLAUDE.md +2 -2
  21. package/agents/clean-coder.md +58 -548
  22. package/agents/code-quality-agent.md +10 -2
  23. package/agents/code-verifier.md +1 -1
  24. package/agents/test_agent_frontmatter.py +32 -40
  25. package/audit-rubrics/CLAUDE.md +2 -1
  26. package/audit-rubrics/audit-categories.json +704 -0
  27. package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
  28. package/bin/CLAUDE.md +16 -5
  29. package/bin/ever-shipped-skills.mjs +2 -0
  30. package/bin/install-plan.mjs +402 -0
  31. package/bin/install-transaction.mjs +455 -0
  32. package/bin/install.mjs +593 -147
  33. package/bin/install.plan.test.mjs +194 -0
  34. package/bin/install.profile-root.test.mjs +154 -0
  35. package/bin/install.profiles.test.mjs +253 -0
  36. package/bin/install.settings-defaults.test.mjs +200 -0
  37. package/bin/install.transaction.test.mjs +400 -0
  38. package/bin/install.uninstall-transaction.test.mjs +418 -0
  39. package/bin/merge_managed_permissions.mjs +130 -0
  40. package/bin/resolve-install-root.mjs +181 -0
  41. package/bin/select-install-targets.mjs +401 -0
  42. package/commands/CLAUDE.md +0 -2
  43. package/docs/references/CLAUDE.md +1 -0
  44. package/docs/references/advisor-tool.md +1 -1
  45. package/docs/references/prose-style-enforcement.md +25 -0
  46. package/hooks/blocking/CLAUDE.md +6 -6
  47. package/hooks/blocking/_path_setup.py +9 -5
  48. package/hooks/blocking/code_rules_docstrings.py +124 -30
  49. package/hooks/blocking/code_rules_enforcer.py +161 -16
  50. package/hooks/blocking/code_rules_shared.py +40 -23
  51. package/hooks/blocking/config/CLAUDE.md +3 -5
  52. package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
  53. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
  54. package/hooks/blocking/eli11_reply_enforcer.py +70 -113
  55. package/hooks/blocking/hedging_language_blocker.py +103 -20
  56. package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
  57. package/hooks/blocking/intent_only_ending_blocker.py +6 -0
  58. package/hooks/blocking/plain_language_blocker.py +139 -20
  59. package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
  60. package/hooks/blocking/state_description_blocker.py +7 -1
  61. package/hooks/blocking/tdd_enforcer.py +8 -0
  62. package/hooks/blocking/test__path_setup.py +28 -0
  63. package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
  64. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
  65. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
  66. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
  67. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
  68. package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
  69. package/hooks/blocking/test_hedging_language_blocker.py +120 -1
  70. package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
  71. package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
  72. package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
  73. package/hooks/blocking/test_plain_language_blocker.py +129 -19
  74. package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
  75. package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
  76. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
  77. package/hooks/blocking/test_state_description_blocker.py +45 -2
  78. package/hooks/blocking/test_stop_dispatcher.py +11 -7
  79. package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
  80. package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
  81. package/hooks/hooks.json +15 -0
  82. package/hooks/hooks_constants/CLAUDE.md +14 -3
  83. package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
  84. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
  85. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
  86. package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
  87. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
  88. package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
  89. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
  90. package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
  91. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
  92. package/hooks/hooks_constants/project_paths_reader.py +31 -4
  93. package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
  94. package/hooks/hooks_constants/pytest_invocation.py +354 -0
  95. package/hooks/hooks_constants/session_start_injector.py +163 -0
  96. package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
  97. package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
  98. package/hooks/hooks_constants/shell_command_segments.py +5 -0
  99. package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
  100. package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
  101. package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
  102. package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
  103. package/hooks/hooks_constants/test_session_start_injector.py +168 -0
  104. package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
  105. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
  106. package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
  107. package/hooks/observability/CLAUDE.md +2 -0
  108. package/hooks/observability/prose_matcher_advisory.py +237 -0
  109. package/hooks/observability/test_prose_matcher_advisory.py +143 -0
  110. package/hooks/session/CLAUDE.md +9 -1
  111. package/hooks/session/_path_setup.py +13 -0
  112. package/hooks/session/issue_tracker_session_starter.py +135 -0
  113. package/hooks/session/orchestrator_auto_starter.py +100 -0
  114. package/hooks/session/test__path_setup.py +28 -0
  115. package/hooks/session/test_issue_tracker_session_starter.py +104 -0
  116. package/hooks/session/test_orchestrator_auto_starter.py +99 -0
  117. package/hooks/session/test_working_style_prompt.py +47 -0
  118. package/hooks/session/untracked_repo_detector.py +1 -24
  119. package/hooks/session/working_style_prompt.py +36 -0
  120. package/hooks/validators/_path_setup.py +19 -0
  121. package/hooks/validators/run_all_validators.py +8 -13
  122. package/installable-surfaces.manifest.json +21 -0
  123. package/output-styles/CLAUDE.md +1 -3
  124. package/package.json +4 -2
  125. package/rules/CLAUDE.md +1 -0
  126. package/rules/durable-post-artifacts.md +2 -2
  127. package/rules/eli11-replies.md +6 -1
  128. package/rules/hedging-claims.md +4 -2
  129. package/rules/long-horizon-autonomy.md +3 -1
  130. package/rules/opus5-communication-contract.md +45 -0
  131. package/rules/plain-language.md +2 -2
  132. package/rules/research-mode.md +1 -1
  133. package/scripts/CLAUDE.md +11 -0
  134. package/scripts/Sync-RepoMain.ps1 +215 -0
  135. package/scripts/active_capability_references.py +218 -0
  136. package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
  137. package/scripts/claude_chain_runner.py +394 -6
  138. package/scripts/claude_chain_usage.py +1 -1
  139. package/scripts/codex_compat_materializer.py +105 -85
  140. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
  141. package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
  142. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
  143. package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
  144. package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
  145. package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
  146. package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
  147. package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
  148. package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
  149. package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
  150. package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
  151. package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
  152. package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
  153. package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
  154. package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
  155. package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
  156. package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
  157. package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
  158. package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
  159. package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
  160. package/scripts/spawn_grok_batch.py +3 -0
  161. package/scripts/test_active_capability_references.py +108 -0
  162. package/scripts/test_claude_chain_runner.py +414 -82
  163. package/scripts/test_claude_chain_usage.py +12 -12
  164. package/scripts/test_resolve_worker_spawn.py +2 -2
  165. package/scripts/test_verify_installable_package.py +208 -0
  166. package/scripts/tests/test_codex_compat_materializer.py +33 -0
  167. package/scripts/verify_installable_package.py +612 -0
  168. package/settings.json +10 -0
  169. package/skills/CLAUDE.md +2 -0
  170. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  171. package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
  172. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
  173. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
  174. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
  175. package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
  176. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
  177. package/skills/autoconverge/reference/convergence.md +2 -1
  178. package/skills/autoconverge/reference/stop-conditions.md +5 -3
  179. package/skills/beat-sheet/SKILL.md +54 -0
  180. package/skills/beat-sheet/reference/visual-beats.md +29 -0
  181. package/skills/bugteam/CONSTRAINTS.md +4 -4
  182. package/skills/bugteam/EXAMPLES.md +1 -1
  183. package/skills/bugteam/reference/README.md +1 -1
  184. package/skills/e-code-review/SKILL.md +26 -5
  185. package/skills/e-code-review/reference/effort-evaluation.md +35 -0
  186. package/skills/e-code-review/reference/medium.md +15 -4
  187. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
  188. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
  189. package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
  190. package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
  191. package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
  192. package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
  193. package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
  194. package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
  195. package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
  196. package/skills/e-code-review/scripts/grok_code_review.py +16 -7
  197. package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
  198. package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
  199. package/skills/e-code-review/scripts/test_grok_code_review.py +77 -0
  200. package/skills/plan-to-pr/scripts/create_packet.py +4 -4
  201. package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
  202. package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
  203. package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
  204. package/skills/plan-to-pr/scripts/validate_run.py +4 -1
  205. package/skills/pr-converge/scripts/check_convergence.py +21 -19
  206. package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
  207. package/skills/pr-converge/scripts/conftest.py +35 -0
  208. package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
  209. package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
  210. package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
  211. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  212. package/skills/rebase/SKILL.md +15 -3
  213. package/skills/reviewer-gates/SKILL.md +2 -2
  214. package/skills/show/SKILL.md +51 -0
  215. package/skills/show/references/accessibility.md +7 -0
  216. package/skills/show/references/art.md +3 -0
  217. package/skills/show/references/charts.md +3 -0
  218. package/skills/show/references/core-design.md +14 -0
  219. package/skills/show/references/erds.md +3 -0
  220. package/skills/show/references/flowcharts.md +3 -0
  221. package/skills/show/references/host-and-html.md +3 -0
  222. package/skills/show/references/illustrative-diagrams.md +10 -0
  223. package/skills/show/references/interaction.md +3 -0
  224. package/skills/show/references/mockups.md +3 -0
  225. package/skills/show/references/quality-gates.md +7 -0
  226. package/skills/show/references/structural-diagrams.md +3 -0
  227. package/skills/show/references/subject-inventory.md +21 -0
  228. package/skills/show/references/svg-contract.md +22 -0
  229. package/skills/show/routing.yaml +30 -0
  230. package/skills/show/samples/pr1262-v2.svg +222 -0
  231. package/skills/show/scripts/README.md +6 -0
  232. package/skills/show/scripts/validate-artifact.py +91 -0
  233. package/skills/show/scripts/validate-package.py +18 -0
  234. package/skills/show/templates/html-widget.html +4 -0
  235. package/skills/show/templates/svg-base.svg +19 -0
  236. package/skills/show/tests/fixtures/css-var.svg +6 -0
  237. package/skills/show/tests/fixtures/dead-ref.svg +7 -0
  238. package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
  239. package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
  240. package/skills/show/tests/fixtures/invalid.svg +1 -0
  241. package/skills/show/tests/fixtures/large-canvas.svg +21 -0
  242. package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
  243. package/skills/show/tests/fixtures/valid.html +1 -0
  244. package/skills/show/tests/test_validate-artifact.py +74 -0
  245. package/skills/show/tests/test_validators.py +59 -0
  246. package/skills/show/workflows/create-visual.md +13 -0
  247. package/skills/show/workflows/review-visual.md +20 -0
  248. package/skills/split-pr/SKILL.md +85 -0
  249. package/skills/split-pr/reference/path-layers.md +16 -0
  250. package/skills/split-pr/reference/proposal-format.md +15 -0
  251. package/skills/split-pr/reference/split-further-loop.md +10 -0
  252. package/skills/split-pr/reference/splitting-principles.md +26 -0
  253. package/skills/split-pr/scripts/analyze_pr.py +279 -0
  254. package/skills/split-pr/scripts/categorize_files.py +106 -0
  255. package/skills/split-pr/scripts/config/__init__.py +1 -0
  256. package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
  257. package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
  258. package/skills/split-pr/scripts/config/packing_constants.py +61 -0
  259. package/skills/split-pr/scripts/config/plan_constants.py +49 -0
  260. package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
  261. package/skills/split-pr/scripts/execute_split_slices.py +82 -0
  262. package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
  263. package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
  264. package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
  265. package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
  266. package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
  267. package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
  268. package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
  269. package/skills/split-pr/scripts/split_pr_title.py +41 -0
  270. package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
  271. package/skills/split-pr/scripts/test_categorize_files.py +55 -0
  272. package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
  273. package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
  274. package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
  275. package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
  276. package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
  277. package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
  278. package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
  279. package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
  280. package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
  281. package/skills/split-pr/scripts/test_verify_plan.py +56 -0
  282. package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
  283. package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
  284. package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
  285. package/skills/split-pr/scripts/verify_plan.py +139 -0
  286. package/system-prompts/software-engineer.xml +11 -2
  287. package/commands/initialize.md +0 -90
  288. package/commands/stubcheck.md +0 -88
  289. package/output-styles/caveman-agent.md +0 -37
@@ -1,13 +1,17 @@
1
- """Add the hooks directory to sys.path for sibling entry-point dispatchers.
1
+ """Add the hooks and blocking directories to sys.path for sibling imports.
2
2
 
3
3
  Importing this module inserts the hooks directory (this file's parent's parent)
4
- at the front of sys.path so a dispatcher that runs as a standalone script from
5
- blocking/ can import hooks_constants with every import kept at module top.
4
+ and the blocking directory (its own parent) at the front of sys.path, so a
5
+ module that runs as a standalone script from blocking/ can import both
6
+ ``hooks_constants`` and its blocking-directory siblings while every import stays
7
+ at module top.
6
8
  """
7
9
 
8
10
  import sys
9
11
  from pathlib import Path
10
12
 
13
+ _blocking_directory = str(Path(__file__).resolve().parent)
11
14
  _hooks_directory = str(Path(__file__).resolve().parent.parent)
12
- if _hooks_directory not in sys.path:
13
- sys.path.insert(0, _hooks_directory)
15
+ for each_directory in (_blocking_directory, _hooks_directory):
16
+ if each_directory not in sys.path:
17
+ sys.path.insert(0, each_directory)
@@ -13,6 +13,7 @@ if _hooks_directory not in sys.path:
13
13
  sys.path.insert(0, _hooks_directory)
14
14
 
15
15
  from code_rules_shared import ( # noqa: E402
16
+ _scope_violations_to_changed_lines,
16
17
  _statement_is_docstring,
17
18
  _walk_skipping_nested_functions,
18
19
  _walk_skipping_type_checking_blocks,
@@ -2664,18 +2665,53 @@ def _runon_sentences(narrative_text: str) -> list[str]:
2664
2665
  return flagged_sentences
2665
2666
 
2666
2667
 
2668
+ def _docstring_owner_span(owner_node: ast.AST, anchor_lineno: int) -> range:
2669
+ """Lines from the owner's anchor through the end of its docstring statement.
2670
+
2671
+ ::
2672
+
2673
+ def clean_helper() -> str: <- anchor (def / class line)
2674
+ '''run-on narrative across lines''' <- docstring end
2675
+ return "ok" <- outside the span
2676
+
2677
+ An edit that only rewrites the docstring body intersects this span and
2678
+ re-grades the finding. An edit to the return line does not.
2679
+ """
2680
+ body = getattr(owner_node, "body", None) or []
2681
+ if not body:
2682
+ return range(anchor_lineno, anchor_lineno + 1)
2683
+ first_statement = body[0]
2684
+ end_lineno = getattr(first_statement, "end_lineno", None) or first_statement.lineno
2685
+ return range(anchor_lineno, end_lineno + 1)
2686
+
2687
+
2667
2688
  def _documentable_docstring_targets(
2668
2689
  parsed_tree: ast.Module,
2669
- ) -> list[tuple[int, str, str]]:
2670
- documentable_targets: list[tuple[int, str, str]] = []
2690
+ ) -> list[tuple[int, str, str, range]]:
2691
+ documentable_targets: list[tuple[int, str, str, range]] = []
2671
2692
  module_docstring = ast.get_docstring(parsed_tree)
2672
2693
  if module_docstring and parsed_tree.body:
2673
- documentable_targets.append((parsed_tree.body[0].lineno, "module", module_docstring))
2694
+ module_anchor = parsed_tree.body[0].lineno
2695
+ documentable_targets.append(
2696
+ (
2697
+ module_anchor,
2698
+ "module",
2699
+ module_docstring,
2700
+ _docstring_owner_span(parsed_tree, module_anchor),
2701
+ )
2702
+ )
2674
2703
  for each_node in _walk_skipping_type_checking_blocks(parsed_tree):
2675
2704
  if isinstance(each_node, ast.ClassDef):
2676
2705
  class_docstring = ast.get_docstring(each_node)
2677
2706
  if class_docstring:
2678
- documentable_targets.append((each_node.lineno, each_node.name, class_docstring))
2707
+ documentable_targets.append(
2708
+ (
2709
+ each_node.lineno,
2710
+ each_node.name,
2711
+ class_docstring,
2712
+ _docstring_owner_span(each_node, each_node.lineno),
2713
+ )
2714
+ )
2679
2715
  continue
2680
2716
  if not isinstance(each_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
2681
2717
  continue
@@ -2686,34 +2722,58 @@ def _documentable_docstring_targets(
2686
2722
  function_docstring = _function_docstring_text(each_node)
2687
2723
  if function_docstring:
2688
2724
  documentable_targets.append(
2689
- (each_node.lineno, f"{each_node.name}()", function_docstring)
2725
+ (
2726
+ each_node.lineno,
2727
+ f"{each_node.name}()",
2728
+ function_docstring,
2729
+ _docstring_owner_span(each_node, each_node.lineno),
2730
+ )
2690
2731
  )
2691
2732
  return documentable_targets
2692
2733
 
2693
2734
 
2694
- def check_docstring_runon_sentence(content: str, file_path: str) -> list[str]:
2735
+ def check_docstring_runon_sentence(
2736
+ content: str,
2737
+ file_path: str,
2738
+ all_changed_lines: set[int] | None = None,
2739
+ defer_scope_to_caller: bool = False,
2740
+ ) -> list[str]:
2695
2741
  """Flag a docstring narrative sentence that reads as a dense run-on wall.
2696
2742
 
2697
2743
  A readable docstring breaks its narrative into short sentences a general
2698
2744
  developer follows on the first read. The one mechanical mark of a wall is a
2699
2745
  single sentence that runs past the word limit while chaining clauses with an
2700
2746
  em-dash, a double-hyphen, or a semicolon. This check inspects the narrative prose of module,
2701
- class, and public-function docstrings the text before the first structured
2747
+ class, and public-function docstrings - the text before the first structured
2702
2748
  section header (``Args:``, ``Arguments:``, ``Returns:``, ``Yields:``,
2703
- ``Raises:``, ``Note:``, ``Notes:``, ``Example:``, or ``Examples:``) and
2749
+ ``Raises:``, ``Note:``, ``Notes:``, ``Example:``, or ``Examples:``) - and
2704
2750
  reports a sentence that is both over the word limit and joined by one of those
2705
2751
  marks.
2706
2752
  Whether the prose paints a concrete, illustrative picture is judgment the
2707
2753
  plain-illustrative-docstrings audit lane carries; this gate catches only the
2708
2754
  run-on mark.
2709
2755
 
2756
+ The caller passes the reconstructed full file as *content* so ``ast.parse``
2757
+ sees a complete module. Findings are then scoped to *all_changed_lines* so an
2758
+ Edit blocks on a run-on it just introduced while a pre-existing far-away
2759
+ run-on on an untouched definition does not block the edit.
2760
+
2710
2761
  Args:
2711
- content: The source text to inspect.
2762
+ content: The source text to inspect - the reconstructed full file on an
2763
+ Edit so the parse succeeds.
2712
2764
  file_path: The path the source will be written to, used for exemptions.
2765
+ all_changed_lines: Post-edit line numbers the current edit touched, or
2766
+ None to treat the whole file as in scope. When provided, a finding
2767
+ blocks only when its definition-through-docstring span intersects
2768
+ the changed lines.
2769
+ defer_scope_to_caller: When True, return every violation so the
2770
+ commit/push gate's ``split_violations_by_scope`` can scope by added
2771
+ line.
2713
2772
 
2714
2773
  Returns:
2715
2774
  One issue per docstring whose narrative carries a run-on sentence, capped
2716
- at the module limit.
2775
+ at the module limit and scoped to the changed lines unless
2776
+ *defer_scope_to_caller* is True or *all_changed_lines* is None.
2717
2777
  """
2718
2778
  if is_test_file(file_path) or is_hook_infrastructure(file_path):
2719
2779
  return []
@@ -2721,25 +2781,37 @@ def check_docstring_runon_sentence(content: str, file_path: str) -> list[str]:
2721
2781
  parsed_tree = ast.parse(content)
2722
2782
  except SyntaxError:
2723
2783
  return []
2724
- issues: list[str] = []
2725
- for each_line_number, each_label, each_docstring in _documentable_docstring_targets(
2726
- parsed_tree
2727
- ):
2784
+ all_violations_in_walk_order: list[tuple[range, str]] = []
2785
+ for (
2786
+ each_line_number,
2787
+ each_label,
2788
+ each_docstring,
2789
+ each_span,
2790
+ ) in _documentable_docstring_targets(parsed_tree):
2728
2791
  flagged_sentences = _runon_sentences(_docstring_narrative_text(each_docstring))
2729
2792
  if not flagged_sentences:
2730
2793
  continue
2731
2794
  run_on_word_count = _sentence_word_count(flagged_sentences[0])
2732
- issues.append(
2795
+ message = (
2733
2796
  f"Line {each_line_number}: {each_label} docstring carries a {run_on_word_count}-word "
2734
- "run-on sentence break the narrative into short, illustrative sentences a general "
2797
+ "run-on sentence - break the narrative into short, illustrative sentences a general "
2735
2798
  "developer reads in one pass (plain-illustrative-docstrings)"
2736
2799
  )
2737
- if len(issues) >= MAX_DOCSTRING_RUNON_SENTENCE_ISSUES:
2738
- break
2739
- return issues[:MAX_DOCSTRING_RUNON_SENTENCE_ISSUES]
2800
+ all_violations_in_walk_order.append((each_span, message))
2801
+ scoped_issues = _scope_violations_to_changed_lines(
2802
+ all_violations_in_walk_order,
2803
+ all_changed_lines,
2804
+ defer_scope_to_caller,
2805
+ )
2806
+ return scoped_issues[:MAX_DOCSTRING_RUNON_SENTENCE_ISSUES]
2740
2807
 
2741
2808
 
2742
- def check_docstring_prose_wall_without_illustration(content: str, file_path: str) -> list[str]:
2809
+ def check_docstring_prose_wall_without_illustration(
2810
+ content: str,
2811
+ file_path: str,
2812
+ all_changed_lines: set[int] | None = None,
2813
+ defer_scope_to_caller: bool = False,
2814
+ ) -> list[str]:
2743
2815
  """Flag a summary that tells for many sentences and shows nothing.
2744
2816
 
2745
2817
  A reader trusts the opening to paint a scene. A run of short sentences with
@@ -2754,13 +2826,28 @@ def check_docstring_prose_wall_without_illustration(content: str, file_path: str
2754
2826
  Past the prose-line limit with no ``::`` listing and no ``>>>`` doctest, this
2755
2827
  fires. A narrative that shows a worked example, or one at the limit, passes.
2756
2828
 
2829
+ The caller passes the reconstructed full file as *content* so ``ast.parse``
2830
+ sees a complete module. Findings are then scoped to *all_changed_lines* so an
2831
+ Edit blocks on a wall it just introduced while a pre-existing far-away wall
2832
+ on an untouched definition does not block the edit.
2833
+
2757
2834
  Args:
2758
- content: The source text to inspect.
2835
+ content: The source text to inspect - the reconstructed full file on an
2836
+ Edit so the parse succeeds.
2759
2837
  file_path: The path the source will be written to, used for exemptions.
2838
+ all_changed_lines: Post-edit line numbers the current edit touched, or
2839
+ None to treat the whole file as in scope. When provided, a finding
2840
+ blocks only when its definition-through-docstring span intersects
2841
+ the changed lines.
2842
+ defer_scope_to_caller: When True, return every violation so the
2843
+ commit/push gate's ``split_violations_by_scope`` can scope by added
2844
+ line.
2760
2845
 
2761
2846
  Returns:
2762
2847
  One issue per summary that runs a wall of sentences with no worked
2763
- example, capped at the issue limit for the rule.
2848
+ example, capped at the issue limit for the rule and scoped to the
2849
+ changed lines unless *defer_scope_to_caller* is True or
2850
+ *all_changed_lines* is None.
2764
2851
  """
2765
2852
  if is_test_file(file_path) or is_hook_infrastructure(file_path):
2766
2853
  return []
@@ -2768,25 +2855,32 @@ def check_docstring_prose_wall_without_illustration(content: str, file_path: str
2768
2855
  parsed_tree = ast.parse(content)
2769
2856
  except SyntaxError:
2770
2857
  return []
2771
- issues: list[str] = []
2772
- for each_line_number, each_label, each_docstring in _documentable_docstring_targets(
2773
- parsed_tree
2774
- ):
2858
+ all_violations_in_walk_order: list[tuple[range, str]] = []
2859
+ for (
2860
+ each_line_number,
2861
+ each_label,
2862
+ each_docstring,
2863
+ each_span,
2864
+ ) in _documentable_docstring_targets(parsed_tree):
2775
2865
  prose_lines, has_illustration = _docstring_narrative_partition(each_docstring)
2776
2866
  if has_illustration:
2777
2867
  continue
2778
2868
  prose_line_count = len(prose_lines)
2779
2869
  if prose_line_count <= DOCSTRING_NARRATIVE_PROSE_LINE_LIMIT:
2780
2870
  continue
2781
- issues.append(
2871
+ message = (
2782
2872
  f"Line {each_line_number}: {each_label} summary runs {prose_line_count} "
2783
2873
  "narrative lines with no worked example - show, don't tell: swap the wall for a "
2784
2874
  "'::' listing (a sample input, an annotated outcome, ok/flag contrast rows) and "
2785
2875
  "keep the narrative to a few short lines (plain-illustrative-docstrings)"
2786
2876
  )
2787
- if len(issues) >= MAX_DOCSTRING_PROSE_WALL_ISSUES:
2788
- break
2789
- return issues[:MAX_DOCSTRING_PROSE_WALL_ISSUES]
2877
+ all_violations_in_walk_order.append((each_span, message))
2878
+ scoped_issues = _scope_violations_to_changed_lines(
2879
+ all_violations_in_walk_order,
2880
+ all_changed_lines,
2881
+ defer_scope_to_caller,
2882
+ )
2883
+ return scoped_issues[:MAX_DOCSTRING_PROSE_WALL_ISSUES]
2790
2884
 
2791
2885
 
2792
2886
  def _raises_section_text(docstring_text: str) -> str:
@@ -18,6 +18,7 @@ concern focused. The separate ``tdd_enforcer.py`` hook accepts any
18
18
  import json
19
19
  import sys
20
20
  from collections import Counter
21
+ from collections.abc import Callable
21
22
  from pathlib import Path
22
23
  from typing import TextIO
23
24
 
@@ -285,7 +286,15 @@ def validate_content(
285
286
  all_issues.extend(check_logging_adjacent_string_literals(content, file_path))
286
287
  all_issues.extend(check_windows_api_none(content))
287
288
  all_issues.extend(check_naive_datetime_construction(content, file_path))
288
- all_issues.extend(check_magic_values(content, file_path))
289
+ all_issues.extend(
290
+ _fragment_or_deferred_check(
291
+ check_magic_values,
292
+ old_content,
293
+ content,
294
+ file_path,
295
+ defer_scope_to_caller,
296
+ )
297
+ )
289
298
  all_issues.extend(check_fstring_structural_literals(content, file_path))
290
299
  all_issues.extend(check_constants_outside_config(content, file_path))
291
300
  all_issues.extend(check_config_duplicate_path_anchor(content, file_path))
@@ -358,9 +367,21 @@ def validate_content(
358
367
  all_issues.extend(
359
368
  check_class_docstring_names_public_methods(effective_content, file_path)
360
369
  )
361
- all_issues.extend(check_docstring_runon_sentence(effective_content, file_path))
362
370
  all_issues.extend(
363
- check_docstring_prose_wall_without_illustration(effective_content, file_path)
371
+ check_docstring_runon_sentence(
372
+ effective_content,
373
+ file_path,
374
+ all_changed_lines,
375
+ defer_scope_to_caller,
376
+ )
377
+ )
378
+ all_issues.extend(
379
+ check_docstring_prose_wall_without_illustration(
380
+ effective_content,
381
+ file_path,
382
+ all_changed_lines,
383
+ defer_scope_to_caller,
384
+ )
364
385
  )
365
386
  all_issues.extend(
366
387
  check_module_docstring_names_public_checks(effective_content, file_path)
@@ -508,8 +529,24 @@ def validate_content(
508
529
  all_issues.extend(check_polarity_name_contradiction(content, file_path))
509
530
  all_issues.extend(check_inline_literal_collections(content, file_path))
510
531
  all_issues.extend(check_inline_tuple_string_magic(content, file_path))
511
- all_issues.extend(check_join_separator_string_magic(content, file_path))
512
- all_issues.extend(check_string_literal_magic(content, file_path))
532
+ all_issues.extend(
533
+ _fragment_or_deferred_check(
534
+ check_join_separator_string_magic,
535
+ old_content,
536
+ content,
537
+ file_path,
538
+ defer_scope_to_caller,
539
+ )
540
+ )
541
+ all_issues.extend(
542
+ _fragment_or_deferred_check(
543
+ check_string_literal_magic,
544
+ old_content,
545
+ content,
546
+ file_path,
547
+ defer_scope_to_caller,
548
+ )
549
+ )
513
550
  all_issues.extend(check_whitespace_indentation_magic(content, file_path))
514
551
  all_issues.extend(check_orphan_css_classes(effective_content, file_path))
515
552
  check_incomplete_mocks(content, file_path)
@@ -723,6 +760,124 @@ def _without_line_prefix(violation_text: str) -> str:
723
760
  return violation_text
724
761
 
725
762
 
763
+ def _issues_absent_from_prior_bodies(
764
+ all_candidate_issues: list[str],
765
+ all_prior_issues: list[str],
766
+ ) -> list[str]:
767
+ """Return candidates whose message bodies are not covered by prior issues.
768
+
769
+ Matching is line-number-agnostic with per-occurrence accounting: each prior
770
+ entry consumes exactly one candidate carrying the same body.
771
+
772
+ ::
773
+
774
+ prior: ["Line 1: magic 'X'"]
775
+ candidates: ["Line 4: magic 'X'", "Line 9: magic 'Y'"]
776
+ -> ["Line 9: magic 'Y'"]
777
+
778
+ Args:
779
+ all_candidate_issues: Findings from the scan under review.
780
+ all_prior_issues: Findings that already account for a body.
781
+
782
+ Returns:
783
+ Candidates not consumed by a matching prior body.
784
+ """
785
+ remaining_prior_counts = Counter(
786
+ _without_line_prefix(each_issue) for each_issue in all_prior_issues
787
+ )
788
+ all_uncovered_issues: list[str] = []
789
+ for each_issue in all_candidate_issues:
790
+ message_body = _without_line_prefix(each_issue)
791
+ if remaining_prior_counts[message_body] > 0:
792
+ remaining_prior_counts[message_body] -= 1
793
+ continue
794
+ all_uncovered_issues.append(each_issue)
795
+ return all_uncovered_issues
796
+
797
+
798
+ def _fragment_or_deferred_check(
799
+ check_function: Callable[[str, str], list[str]],
800
+ old_content: str,
801
+ new_content: str,
802
+ file_path: str,
803
+ defer_scope_to_caller: bool,
804
+ ) -> list[str]:
805
+ """Run a check with fragment baselining, or full-file when the gate owns scope.
806
+
807
+ The commit/push gate sets ``defer_scope_to_caller`` and passes HEAD as
808
+ ``old_content`` while scanning the current file (often the same blob on a
809
+ clean worktree). Fragment baselining would grandfather every finding there,
810
+ so the gate path runs the check on ``new_content`` alone and classifies by
811
+ added line afterward. PreToolUse Edit keeps baselining against the prior
812
+ fragment.
813
+
814
+ ::
815
+
816
+ defer=True, old==new with magic 9999
817
+ -> [Line 2: Magic value 9999 ...] gate still sees it
818
+ defer=False, old and new both carry the same magic
819
+ -> [] grandfathered for wide Edit
820
+
821
+ Args:
822
+ check_function: A ``(content, file_path) -> list[str]`` check.
823
+ old_content: Prior fragment or gate HEAD blob.
824
+ new_content: Proposed fragment or current full file.
825
+ file_path: Destination path used for path-based exemptions.
826
+ defer_scope_to_caller: True when the gate will scope by added line.
827
+
828
+ Returns:
829
+ Findings from the check, baselined against ``old_content`` only when
830
+ ``defer_scope_to_caller`` is False.
831
+ """
832
+ if defer_scope_to_caller:
833
+ return check_function(new_content, file_path)
834
+ return _issues_introduced_in_fragment(
835
+ check_function, old_content, new_content, file_path
836
+ )
837
+
838
+
839
+ def _issues_introduced_in_fragment(
840
+ check_function: Callable[[str, str], list[str]],
841
+ old_content: str,
842
+ new_content: str,
843
+ file_path: str,
844
+ ) -> list[str]:
845
+ """Return fragment findings that are new relative to the pre-edit fragment.
846
+
847
+ Grades both the prior and proposed Edit fragments with the same check, then
848
+ subtracts findings whose message body already appears in the prior scan.
849
+ Per-occurrence accounting keeps a second identical new finding when only
850
+ one matching body existed before. Line numbers on kept findings come from
851
+ the proposed fragment so diagnostics point at the introduced location.
852
+
853
+ ::
854
+
855
+ old: def f(): return os.environ['STRIPE_SECRET']
856
+ new: def f(): return os.environ['STRIPE_SECRET'] # same body
857
+ -> [] grandfathered
858
+ old: def f(): return 0
859
+ new: def f(): return os.environ['STRIPE_SECRET']
860
+ -> [Line ...: string magic value 'STRIPE_SECRET' ...]
861
+
862
+ Args:
863
+ check_function: A ``(content, file_path) -> list[str]`` check.
864
+ old_content: The Edit's prior ``old_string`` fragment, or empty when
865
+ there is no prior region to baseline against.
866
+ new_content: The Edit's ``new_string`` fragment (or Write body).
867
+ file_path: Destination path used for path-based exemptions.
868
+
869
+ Returns:
870
+ Findings present in ``new_content`` that are absent from ``old_content``
871
+ under line-number-agnostic body matching. When ``old_content`` is empty,
872
+ every finding from ``new_content`` is returned.
873
+ """
874
+ all_new_issues = check_function(new_content, file_path)
875
+ if not old_content:
876
+ return all_new_issues
877
+ all_old_issues = check_function(old_content, file_path)
878
+ return _issues_absent_from_prior_bodies(all_new_issues, all_old_issues)
879
+
880
+
726
881
  def _forecast_full_file_violations(
727
882
  full_file_content_after_edit: str,
728
883
  file_path: str,
@@ -763,17 +918,7 @@ def _forecast_full_file_violations(
763
918
  all_full_file_issues = validate_content(
764
919
  full_file_content_after_edit, file_path, prior_full_file_content
765
920
  )
766
- remaining_blocking_counts = Counter(
767
- _without_line_prefix(each_issue) for each_issue in all_blocking_issues
768
- )
769
- forecast_issues: list[str] = []
770
- for each_issue in all_full_file_issues:
771
- message_body = _without_line_prefix(each_issue)
772
- if remaining_blocking_counts[message_body] > 0:
773
- remaining_blocking_counts[message_body] -= 1
774
- continue
775
- forecast_issues.append(each_issue)
776
- return forecast_issues
921
+ return _issues_absent_from_prior_bodies(all_full_file_issues, all_blocking_issues)
777
922
 
778
923
 
779
924
  def _precheck_hint() -> str:
@@ -3,19 +3,12 @@
3
3
  import ast
4
4
  import difflib
5
5
  import os
6
- import sys
7
6
  import tempfile
8
7
  from collections.abc import Collection, Iterator
9
8
  from pathlib import Path
10
9
 
11
- _blocking_directory = str(Path(__file__).resolve().parent)
12
- _hooks_directory = str(Path(__file__).resolve().parent.parent)
13
- if _blocking_directory not in sys.path:
14
- sys.path.insert(0, _blocking_directory)
15
- if _hooks_directory not in sys.path:
16
- sys.path.insert(0, _hooks_directory)
17
-
18
- from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
10
+ from hooks_constants.code_rules_enforcer_constants import (
11
+ ALL_AGENT_HOME_TOOLING_PATTERNS,
19
12
  ALL_DIFF_CHANGED_OPCODE_TAGS,
20
13
  ALL_EPHEMERAL_EXEMPT_DISABLE_TRUTHY_VALUES,
21
14
  ALL_HOOK_INFRASTRUCTURE_PATTERNS,
@@ -30,14 +23,14 @@ from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
30
23
  LEADING_DRIVE_LETTER_PATTERN,
31
24
  STRICT_TEST_FILE_BASENAME_PATTERN,
32
25
  )
33
- from hooks_constants.harness_scratchpad_constants import ( # noqa: E402
26
+ from hooks_constants.harness_scratchpad_constants import (
34
27
  CLAUDE_SESSION_ID_ENVIRONMENT_VARIABLE_NAME,
35
28
  HARNESS_SCRATCHPAD_LEAF_DIRECTORY_NAME,
36
29
  HARNESS_SCRATCHPAD_USER_DIRECTORY_NAME,
37
30
  HARNESS_SCRATCHPAD_USER_DIRECTORY_PREFIX,
38
31
  HOOK_PAYLOAD_SESSION_ID_KEY,
39
32
  )
40
- from hooks_constants.unused_module_import_constants import ( # noqa: E402
33
+ from hooks_constants.unused_module_import_constants import (
41
34
  TYPE_CHECKING_IDENTIFIER,
42
35
  )
43
36
 
@@ -56,6 +49,27 @@ def is_hook_infrastructure(file_path: str) -> bool:
56
49
  return any(pattern.replace("\\", "/") in path_lower for pattern in ALL_HOOK_INFRASTRUCTURE_PATTERNS)
57
50
 
58
51
 
52
+ def is_agent_home_tooling(file_path: str) -> bool:
53
+ """Check whether a path sits under a coding agent's home-directory tooling.
54
+
55
+ A coding agent installs helper scripts under a dot-directory in the home
56
+ tree, such as ``~/.grok/runs/``. That tree is vendored tooling rather than
57
+ project code, so the repository code rules do not govern it. The leading dot
58
+ is load-bearing: a project directory named ``grok/`` stays governed.
59
+
60
+ Args:
61
+ file_path: The candidate path to classify.
62
+
63
+ Returns:
64
+ True when the path sits under a recognized agent home directory.
65
+ """
66
+ path_lower = "/" + file_path.lower().replace("\\", "/").lstrip("/")
67
+ return any(
68
+ each_pattern.replace("\\", "/") in path_lower
69
+ for each_pattern in ALL_AGENT_HOME_TOOLING_PATTERNS
70
+ )
71
+
72
+
59
73
  def is_test_file(file_path: str) -> bool:
60
74
  """Check if file is a test file."""
61
75
  path_lower = file_path.lower()
@@ -405,17 +419,18 @@ def is_under_session_scratchpad(file_path: str, hook_payload: dict) -> bool:
405
419
 
406
420
 
407
421
  def is_ephemeral_path(file_path: str, hook_payload: dict | None = None) -> bool:
408
- """Return True when file_path is a throwaway scratch path exempt from repo gates.
409
-
410
- Combines the two throwaway-path families a repo gate skips: the root-anchored
411
- ephemeral scratch directories (``/tmp`` and ``$CLAUDE_JOB_DIR/tmp``) and the
412
- harness session scratchpad. The session scratchpad match reads the session id
413
- from the payload when one is supplied, and from the harness environment
414
- variable otherwise, so a caller that holds no payload still gets the match.
415
- The run_all_validators PreToolUse gate calls this predicate to skip a scratch
416
- target before it validates. The code-rules and TDD gates call the two path
417
- predicates ``is_ephemeral_script_path`` and ``is_under_session_scratchpad``
418
- directly.
422
+ """Return True when file_path is exempt from the repository gates.
423
+
424
+ Combines the path families a repo gate skips: the root-anchored ephemeral
425
+ scratch directories (``/tmp`` and ``$CLAUDE_JOB_DIR/tmp``), the harness
426
+ session scratchpad, and a coding agent's own home-directory tooling such as
427
+ ``~/.grok/runs/``. The session scratchpad match reads the session id from the
428
+ payload when one is supplied, and from the harness environment variable
429
+ otherwise, so a caller that holds no payload still gets the match. The
430
+ run_all_validators PreToolUse gate calls this predicate to skip an exempt
431
+ target before it validates. The code-rules and TDD gates call the underlying
432
+ predicates ``is_ephemeral_script_path``, ``is_agent_home_tooling``, and
433
+ ``is_under_session_scratchpad`` directly.
419
434
 
420
435
  Args:
421
436
  file_path: The candidate path to classify.
@@ -423,10 +438,12 @@ def is_ephemeral_path(file_path: str, hook_payload: dict | None = None) -> bool:
423
438
  read the session id from the environment alone.
424
439
 
425
440
  Returns:
426
- True when the path is ephemeral scratch or under the session scratchpad.
441
+ True when the path is scratch, agent tooling, or the session scratchpad.
427
442
  """
428
443
  if is_ephemeral_script_path(file_path):
429
444
  return True
445
+ if is_agent_home_tooling(file_path):
446
+ return True
430
447
  return is_under_session_scratchpad(file_path, hook_payload or {})
431
448
 
432
449
 
@@ -1,16 +1,14 @@
1
1
  # hooks/blocking/config
2
2
 
3
- A Python package that holds shared constants for the verified-commit gate family. Three modules in `blocking/` import from here:
4
-
5
- - `verification_verdict_store.py`
6
- - `verified_commit_gate.py`
7
- - `verifier_verdict_minter.py`
3
+ A Python package that holds shared constants for blocking hooks. Verified-commit gate modules and opinionated prose-style gates import from here.
8
4
 
9
5
  ## Key files
10
6
 
11
7
  | File | Contents |
12
8
  |---|---|
13
9
  | `__init__.py` | Declares this as a regular package (not a namespace package) so it resolves first on `sys.path` |
10
+ | `code_review_enforcement_constants.py` | Stamp and effort tunables for the code-review enforcement gates |
11
+ | `prose_style_enforcement_constants.py` | `CLAUDE_PROSE_STYLE_ENFORCEMENT` opt-in (default off) for opinionated prose gates |
14
12
  | `verified_commit_constants.py` | All tunables for the gate: directory names, regex patterns for detecting verdict paths and obfuscation attempts, timeout values, git subcommand sets, bypass marker, and corrective messages |
15
13
  | `verified_commit_context_constants.py` | `VERIFY_SKIP_ADDITIONAL_CONTEXT`, the ``# verify-skip`` usage rule the gate attaches to a deny payload's `additionalContext` |
16
14
  | `verified_commit_gate_output_constants.py` | `PRE_TOOL_USE_HOOK_EVENT_NAME` and `DENY_PERMISSION_DECISION` (the deny payload's event name and decision string), `GATE_HOOK_MODULE_NAME` (the gate's own module name for block logging), and `REGEX_ALTERNATION_SEPARATOR` (the `\|` join for the directory-change verb alternation in `gated_invocations.py`) |
@@ -0,0 +1,38 @@
1
+ """Single source of truth for opinionated prose-style gate opt-in.
2
+
3
+ ::
4
+
5
+ CLAUDE_PROSE_STYLE_ENFORCEMENT=1 -> PROSE_STYLE_ENFORCEMENT_ENABLED True
6
+ (unset) -> PROSE_STYLE_ENFORCEMENT_ENABLED False
7
+
8
+ Structural AskUserQuestion lean-block validation stays always on. The flag
9
+ arms only opinionated prose gates (heavy-word swaps, hedging, historical
10
+ state phrasing, intent-only endings, hook prose-detector consistency).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+
17
+ PROSE_STYLE_ENFORCEMENT_ENV_VAR = "CLAUDE_PROSE_STYLE_ENFORCEMENT"
18
+ ALL_ENFORCEMENT_ENABLED_ENV_VALUES = frozenset({"1", "true", "yes", "on"})
19
+
20
+
21
+ def prose_style_enforcement_enabled_in_environment() -> bool:
22
+ """Read whether this machine turns opinionated prose gates on.
23
+
24
+ ::
25
+
26
+ CLAUDE_PROSE_STYLE_ENFORCEMENT=1 -> True
27
+ CLAUDE_PROSE_STYLE_ENFORCEMENT=" On " -> True
28
+ CLAUDE_PROSE_STYLE_ENFORCEMENT=0 -> False
29
+ (variable unset) -> False
30
+
31
+ Returns:
32
+ True when the variable holds an enabled value, False otherwise.
33
+ """
34
+ raw_environment_setting = os.environ.get(PROSE_STYLE_ENFORCEMENT_ENV_VAR, "")
35
+ return raw_environment_setting.strip().lower() in ALL_ENFORCEMENT_ENABLED_ENV_VALUES
36
+
37
+
38
+ PROSE_STYLE_ENFORCEMENT_ENABLED = prose_style_enforcement_enabled_in_environment()