claude-dev-env 2.3.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 (290) hide show
  1. package/CLAUDE.md +53 -48
  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 +3 -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 +3 -4
  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/skill-writer-agent.md +84 -0
  31. package/agents/test_agent_frontmatter.py +67 -18
  32. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +105 -3
  33. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
  34. package/bin/CLAUDE.md +68 -5
  35. package/bin/ever-shipped-skills.mjs +1 -0
  36. package/bin/install-constants.mjs +88 -0
  37. package/bin/install.mjs +1138 -114
  38. package/bin/install.prune.test.mjs +869 -19
  39. package/bin/install.test.mjs +906 -2
  40. package/commands/implement.md +1 -1
  41. package/commands/right-size.md +1 -1
  42. package/docs/CLAUDE.md +2 -0
  43. package/docs/host-pool-health-monitor.md +102 -0
  44. package/docs/references/CLAUDE.md +5 -2
  45. package/docs/references/advisor-tool.md +13 -0
  46. package/docs/references/code-review-enforcement.md +107 -0
  47. package/docs/references/team-advisor-skill.md +14 -0
  48. package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
  49. package/hooks/blocking/CLAUDE.md +9 -1
  50. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
  51. package/hooks/blocking/code_review_gate_deny.py +74 -0
  52. package/hooks/blocking/code_review_pr_create_gate.py +198 -0
  53. package/hooks/blocking/code_review_push_gate.py +145 -0
  54. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +348 -0
  55. package/hooks/blocking/code_review_stamp_store.py +233 -0
  56. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
  57. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
  58. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
  59. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
  60. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
  61. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
  62. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
  63. package/hooks/blocking/config/__init__.py +5 -5
  64. package/hooks/blocking/config/code_review_enforcement_constants.py +113 -0
  65. package/hooks/blocking/config/test_code_review_enforcement_constants.py +113 -0
  66. package/hooks/blocking/config/verified_commit_constants.py +160 -155
  67. package/hooks/blocking/conftest.py +2 -0
  68. package/hooks/blocking/convergence_gate_blocker.py +112 -23
  69. package/hooks/blocking/destructive_command_blocker.py +19 -6
  70. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
  71. package/hooks/blocking/pr_description_proof_of_work.py +52 -34
  72. package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
  73. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
  74. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
  75. package/hooks/blocking/test_code_review_gate_deny.py +54 -0
  76. package/hooks/blocking/test_code_review_pr_create_gate.py +199 -0
  77. package/hooks/blocking/test_code_review_push_gate.py +205 -0
  78. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +199 -0
  79. package/hooks/blocking/test_code_review_stamp_store.py +205 -0
  80. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
  81. package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
  82. package/hooks/blocking/test_destructive_command_blocker.py +1 -1
  83. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
  84. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
  85. package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
  86. package/hooks/blocking/test_pre_tool_use_dispatcher.py +17 -8
  87. package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
  88. package/hooks/blocking/test_verification_verdict_store.py +974 -903
  89. package/hooks/blocking/test_verified_commit_gate.py +581 -581
  90. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
  91. package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
  92. package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
  93. package/hooks/blocking/verification_verdict_store.py +1039 -1014
  94. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
  95. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
  96. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
  97. package/hooks/blocking/verifier_verdict_minter.py +280 -280
  98. package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
  99. package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
  100. package/hooks/git-hooks/pre_push.py +89 -2
  101. package/hooks/git-hooks/test_pre_push.py +128 -0
  102. package/hooks/hooks.json +26 -1
  103. package/hooks/hooks_constants/CLAUDE.md +3 -1
  104. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
  105. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  106. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
  107. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
  108. package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
  109. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
  110. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
  111. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
  112. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  113. package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
  114. package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
  115. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
  116. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
  117. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
  118. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
  119. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
  120. package/hooks/pyproject.toml +1 -0
  121. package/hooks/validators/CLAUDE.md +2 -0
  122. package/hooks/validators/config/__init__.py +0 -0
  123. package/hooks/validators/config/directory_exemption_constants.py +183 -0
  124. package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
  125. package/hooks/validators/conftest.py +4 -0
  126. package/hooks/validators/mypy_integration.py +63 -50
  127. package/hooks/validators/pyproject_config_discovery.py +101 -0
  128. package/hooks/validators/ruff_integration.py +257 -25
  129. package/hooks/validators/run_all_validators.py +223 -19
  130. package/hooks/validators/test_directory_exemption_constants.py +185 -0
  131. package/hooks/validators/test_mypy_integration.py +32 -0
  132. package/hooks/validators/test_pyproject_config_discovery.py +94 -0
  133. package/hooks/validators/test_python_antipattern_checks.py +110 -5
  134. package/hooks/validators/test_ruff_integration.py +160 -2
  135. package/hooks/validators/test_run_all_validators.py +140 -68
  136. package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
  137. package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
  138. package/package.json +10 -2
  139. package/rules/CLAUDE.md +1 -0
  140. package/rules/docstring-prose-matches-implementation.md +45 -67
  141. package/rules/durable-post-artifacts.md +7 -0
  142. package/rules/state-what-is.md +25 -0
  143. package/rules/verified-commit-gate-skip.md +1 -1
  144. package/scripts/CLAUDE.md +1 -0
  145. package/scripts/Capture-PoolHealth.ps1 +410 -0
  146. package/scripts/_code_review_test_support.py +404 -0
  147. package/scripts/claude_chain_runner.py +141 -1
  148. package/scripts/codec_forwarding_test_support.py +83 -0
  149. package/scripts/conftest.py +23 -0
  150. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -2
  151. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +53 -1
  152. package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
  153. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
  154. package/scripts/invoke_code_review.py +550 -38
  155. package/scripts/resolve_worker_spawn.py +626 -619
  156. package/scripts/spawn_grok_batch.py +672 -672
  157. package/scripts/test_claude_chain_runner.py +131 -0
  158. package/scripts/test_invoke_code_review_chain.py +70 -0
  159. package/scripts/test_invoke_code_review_cli.py +192 -0
  160. package/scripts/test_invoke_code_review_codec.py +77 -0
  161. package/scripts/test_invoke_code_review_contract.py +256 -0
  162. package/scripts/test_invoke_code_review_git.py +123 -0
  163. package/scripts/test_invoke_code_review_mode.py +99 -0
  164. package/scripts/test_resolve_worker_spawn.py +1014 -1014
  165. package/scripts/test_resolve_worker_spawn_codec.py +101 -0
  166. package/skills/CLAUDE.md +2 -0
  167. package/skills/auditing-claude-config/SKILL.md +114 -114
  168. package/skills/autoconverge/SKILL.md +427 -421
  169. package/skills/autoconverge/reference/convergence.md +26 -4
  170. package/skills/autoconverge/reference/multi-pr.md +6 -1
  171. package/skills/autoconverge/reference/stop-conditions.md +16 -10
  172. package/skills/autoconverge/workflow/CLAUDE.md +1 -0
  173. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
  174. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
  175. package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1244
  176. package/skills/autoconverge/workflow/converge.mjs +191 -8
  177. package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
  178. package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
  179. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
  180. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
  181. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
  182. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
  183. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
  184. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
  185. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
  186. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
  187. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
  188. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
  189. package/skills/closeout/SKILL.md +33 -50
  190. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
  191. package/skills/codex-review/scripts/run_codex_review.py +233 -1
  192. package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
  193. package/skills/condensing-instructions/SKILL.md +81 -0
  194. package/skills/copilot-review/SKILL.md +119 -119
  195. package/skills/e-code-review/SKILL.md +52 -0
  196. package/skills/e-code-review/reference/fix.md +54 -0
  197. package/skills/e-code-review/reference/loop.md +43 -0
  198. package/skills/e-code-review/reference/low.md +57 -0
  199. package/skills/e-code-review/reference/medium.md +153 -0
  200. package/skills/e-code-review/reference/xhigh.md +182 -0
  201. package/skills/e-simplify/SKILL.md +97 -0
  202. package/skills/fresh-branch/CLAUDE.md +2 -0
  203. package/skills/fresh-branch/SKILL.md +2 -0
  204. package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
  205. package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
  206. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
  207. package/skills/fresh-branch/scripts/pytest.ini +4 -0
  208. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
  209. package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
  210. package/skills/issue-tracker/SKILL.md +92 -0
  211. package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
  212. package/skills/issue-tracker/reference/handoff-schema.md +64 -0
  213. package/skills/issue-tracker/reference/operation-matrix.md +41 -0
  214. package/skills/orchestrator/SKILL.md +162 -21
  215. package/skills/orchestrator/scripts/status_gate.py +625 -0
  216. package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
  217. package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
  218. package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
  219. package/skills/orchestrator/scripts/test_status_gate.py +439 -0
  220. package/skills/orchestrator-refresh/SKILL.md +110 -35
  221. package/skills/plan-to-pr/SKILL.md +155 -0
  222. package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
  223. package/skills/plan-to-pr/reference/model-routing.md +36 -0
  224. package/skills/plan-to-pr/reference/packet-contract.md +43 -0
  225. package/skills/plan-to-pr/reference/packet-schema.json +57 -0
  226. package/skills/plan-to-pr/reference/process-inventory.md +22 -0
  227. package/skills/plan-to-pr/reference/review-loop.md +33 -0
  228. package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
  229. package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
  230. package/skills/plan-to-pr/reference/task-seeds.md +14 -0
  231. package/skills/plan-to-pr/reference/task-ticket.md +38 -0
  232. package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
  233. package/skills/plan-to-pr/scripts/config/constants.py +193 -0
  234. package/skills/plan-to-pr/scripts/create_packet.py +173 -0
  235. package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
  236. package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
  237. package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
  238. package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
  239. package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
  240. package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
  241. package/skills/plan-to-pr/scripts/validate_run.py +173 -0
  242. package/skills/plan-to-pr/test_skill_contract.py +207 -0
  243. package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
  244. package/skills/pr-converge/SKILL.md +472 -469
  245. package/skills/pr-converge/reference/examples.md +3 -3
  246. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  247. package/skills/pr-converge/reference/ground-rules.md +7 -4
  248. package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
  249. package/skills/pr-converge/reference/per-tick.md +5 -5
  250. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  251. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
  252. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
  253. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
  254. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  255. package/skills/pr-fix-protocol/SKILL.md +1 -1
  256. package/skills/privacy-hygiene/SKILL.md +68 -68
  257. package/skills/prototype/SKILL.md +86 -0
  258. package/skills/prototype/reference/honest-limitations.md +23 -0
  259. package/skills/prototype/reference/promotion-tasks.md +23 -0
  260. package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
  261. package/skills/prototype/scripts/conftest.py +15 -0
  262. package/skills/prototype/scripts/launch_sandbox.py +205 -0
  263. package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
  264. package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
  265. package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
  266. package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
  267. package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
  268. package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
  269. package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
  270. package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
  271. package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
  272. package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
  273. package/skills/prototype/workflows/promotion.md +27 -0
  274. package/skills/prototype/workflows/sandbox.md +35 -0
  275. package/skills/release-notes-html/SKILL.md +164 -0
  276. package/skills/skill-builder/CLAUDE.md +3 -3
  277. package/skills/skill-builder/SKILL.md +5 -5
  278. package/skills/skill-builder/references/CLAUDE.md +1 -1
  279. package/skills/skill-builder/references/delegation-map.md +3 -3
  280. package/skills/skill-builder/references/description-field.md +1 -1
  281. package/skills/skill-builder/references/skill-modularity.md +2 -3
  282. package/skills/skill-builder/workflows/CLAUDE.md +1 -1
  283. package/skills/skill-builder/workflows/improve-skill.md +1 -1
  284. package/skills/skill-builder/workflows/new-skill.md +2 -2
  285. package/skills/task-build/CLAUDE.md +8 -7
  286. package/skills/task-build/SKILL.md +16 -8
  287. package/skills/task-build/reference/tool-routing.md +19 -0
  288. package/skills/team-advisor/SKILL.md +2 -2
  289. package/scripts/test_invoke_code_review.py +0 -672
  290. package/skills/closeout/reference/issue-body-templates.md +0 -108
@@ -81,14 +81,15 @@ def _resolved_option_value(
81
81
  return value_after_option(all_following_tokens, option_index)
82
82
 
83
83
 
84
- def gated_invocation_directory(all_following_tokens: list[str]) -> tuple[bool, str | None]:
84
+ def gated_invocation_directory(
85
+ all_following_tokens: list[str],
86
+ all_gated_subcommands: frozenset[str] = GATED_GIT_SUBCOMMANDS,
87
+ ) -> tuple[bool, str | None]:
85
88
  """Walk the quote-stripped tokens after a ``git`` word to its subcommand.
86
89
 
87
- ``git stash push`` is not gated ("push" is an argument); ``git commit -m
88
- x`` is gated ("commit" sits in subcommand position).
89
-
90
- Returns:
91
- Whether the subcommand is gated, and its ``-C``/``--work-tree`` directory.
90
+ Args:
91
+ all_following_tokens: Quote-stripped tokens after the ``git`` word.
92
+ all_gated_subcommands: Subcommand names that gate; defaults to commit+push.
92
93
  """
93
94
  repo_directory: str | None = None
94
95
  work_tree_directory: str | None = None
@@ -108,7 +109,7 @@ def gated_invocation_directory(all_following_tokens: list[str]) -> tuple[bool, s
108
109
  if each_token.startswith("-"):
109
110
  token_index += 1
110
111
  continue
111
- return each_token.lower() in GATED_GIT_SUBCOMMANDS, repo_directory or work_tree_directory
112
+ return each_token.lower() in all_gated_subcommands, repo_directory or work_tree_directory
112
113
  return False, repo_directory or work_tree_directory
113
114
 
114
115
 
@@ -167,11 +168,16 @@ def _apply_directory_change(
167
168
 
168
169
 
169
170
  def _target_directory_for_match(
170
- command_text: str, git_word_match: re.Match[str], active_directory: str
171
+ command_text: str,
172
+ git_word_match: re.Match[str],
173
+ active_directory: str,
174
+ all_gated_subcommands: frozenset[str],
171
175
  ) -> str | None:
172
176
  """Resolve the gated directory for one ``git`` word match, or None."""
173
177
  all_following_tokens = _following_tokens(command_text, git_word_match.end())
174
- is_gated, flagged_directory = gated_invocation_directory(all_following_tokens)
178
+ is_gated, flagged_directory = gated_invocation_directory(
179
+ all_following_tokens, all_gated_subcommands
180
+ )
175
181
  if not is_gated:
176
182
  return None
177
183
  if flagged_directory is None:
@@ -179,18 +185,22 @@ def _target_directory_for_match(
179
185
  return resolve_against(active_directory, flagged_directory)
180
186
 
181
187
 
182
- def gated_repo_directories(command_text: str, fallback_directory: str) -> list[str]:
183
- """Collect the directories of every git commit/push found in a command.
188
+ def gated_repo_directories(
189
+ command_text: str,
190
+ fallback_directory: str,
191
+ all_gated_subcommands: frozenset[str] = GATED_GIT_SUBCOMMANDS,
192
+ ) -> list[str]:
193
+ """Collect the directories of every gated git call found in a command.
184
194
 
185
195
  Args:
186
196
  command_text: The raw command string from the tool payload.
187
- fallback_directory: The session working directory, used as the
188
- active directory until a directory-change verb changes it and
189
- when the git call carries no ``-C`` flag.
197
+ fallback_directory: The session working directory, the active directory
198
+ until a directory-change verb or a ``-C`` flag overrides it.
199
+ all_gated_subcommands: Subcommand names that gate; defaults to commit+push.
190
200
 
191
201
  Returns:
192
- One directory per detected commit/push invocation, in order; empty
193
- when the command carries no gated git verb.
202
+ One directory per detected gated invocation, in order; empty when the
203
+ command carries no gated git verb.
194
204
  """
195
205
  command_text = collapse_line_continuations(command_text)
196
206
  active_directory = fallback_directory
@@ -199,7 +209,9 @@ def gated_repo_directories(command_text: str, fallback_directory: str) -> list[s
199
209
  if each_match.group().lower().strip("\"'") in DIRECTORY_CHANGE_VERBS:
200
210
  active_directory = _apply_directory_change(command_text, each_match, active_directory)
201
211
  continue
202
- target_directory = _target_directory_for_match(command_text, each_match, active_directory)
212
+ target_directory = _target_directory_for_match(
213
+ command_text, each_match, active_directory, all_gated_subcommands
214
+ )
203
215
  if target_directory is not None:
204
216
  target_directories.append(target_directory)
205
217
  return target_directories
@@ -59,3 +59,38 @@ def test_gated_repo_directories_gates_an_empty_work_tree_against_the_session() -
59
59
  assert gated_repo_directories("git --work-tree= commit -m x", "/session") == [
60
60
  os.path.join("/session", "")
61
61
  ]
62
+
63
+
64
+ def test_default_gated_subcommands_gates_both_commit_and_push() -> None:
65
+ assert gated_repo_directories("git commit -m x", "/session") == ["/session"]
66
+ assert gated_repo_directories("git push", "/session") == ["/session"]
67
+
68
+
69
+ def test_push_only_subcommands_gate_push_but_not_commit() -> None:
70
+ push_only = frozenset({"push"})
71
+ assert gated_invocation_directory(["push"], push_only)[0] is True
72
+ assert gated_invocation_directory(["commit", "-m", "x"], push_only)[0] is False
73
+
74
+
75
+ def test_commit_only_subcommands_gate_commit_but_not_push() -> None:
76
+ commit_only = frozenset({"commit"})
77
+ assert gated_invocation_directory(["commit", "-m", "x"], commit_only)[0] is True
78
+ assert gated_invocation_directory(["push"], commit_only)[0] is False
79
+
80
+
81
+ def test_gated_repo_directories_threads_push_only_subcommands() -> None:
82
+ push_only = frozenset({"push"})
83
+ assert gated_repo_directories("git push", "/session", push_only) == ["/session"]
84
+ assert gated_repo_directories("git commit -m x", "/session", push_only) == []
85
+
86
+
87
+ def test_gated_repo_directories_accepts_the_all_gated_subcommands_keyword() -> None:
88
+ push_only = frozenset({"push"})
89
+ assert gated_repo_directories(
90
+ "git push", "/session", all_gated_subcommands=push_only
91
+ ) == ["/session"]
92
+
93
+
94
+ def test_gated_invocation_directory_accepts_the_all_gated_subcommands_keyword() -> None:
95
+ push_only = frozenset({"push"})
96
+ assert gated_invocation_directory(["push"], all_gated_subcommands=push_only)[0] is True
@@ -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()