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
@@ -1,155 +1,160 @@
1
- """Constants for the verified-commit gate hook family.
2
-
3
- Shared by ``verification_verdict_store.py``, ``verified_commit_gate.py``,
4
- and ``verifier_verdict_minter.py`` so every tunable lives in one place.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- GIT_TIMEOUT_SECONDS = 30
10
- ROOT_KEY_HEX_LENGTH = 16
11
- VERDICT_JSON_INDENT = 2
12
- CLAUDE_HOME_DIRECTORY_NAME = ".claude"
13
- VERDICT_DIRECTORY_NAME = "verification"
14
- VERDICT_DIRECTORY_NAME_SEPARATOR_PATTERN = r"['\"\\/,\s]+"
15
- VERDICT_DIRECTORY_PATH_BOUNDARY_PATTERN = r"(?=['\"]*[\\/,])"
16
- RELATIVE_VERDICT_DIRECTORY_PATTERN = r"(?:^|(?<=[\s;&|(='\"]))verification[\\/]"
17
- VERDICT_PATH_GLUE_PATTERN = r"['\"+\\/\s]*[\\/]['\"+\\/\s]*"
18
- VERDICT_DIRECTORY_CHANGE_TARGET_PATTERN = r"[ \t]+['\"]?verification[\\/]?['\"]?(?=[\s;&|]|$)"
19
- VERDICT_PATH_JOINED_VARIABLE_PATTERN = r"\$\{?(\w+)\}?[\\/]|[\\/]\$\{?(\w+)\}?"
20
- VERDICT_PATH_VARIABLE_ASSIGNMENT_PATTERN = r"(?:^|(?<=[\s;&|(]))%s=(\S+)"
21
- VERDICT_FILE_RELATIVE_REFERENCE_PATTERN = (
22
- rf"(?:^|(?<=[\s;&|(='\"\\/]))verification[\\/][0-9a-f]{{{ROOT_KEY_HEX_LENGTH}}}\.json"
23
- )
24
- PATH_OBFUSCATION_PRIMITIVE_PATTERN = (
25
- r"chr\s*\(|bytes\.fromhex\s*\(|b64decode\s*\(|codecs\.decode\s*\("
26
- r"|(?:bytes|bytearray)\s*\(\s*\[|\[char\[?\]?\]"
27
- )
28
- ALL_VERDICT_PATH_SEGMENT_NAMES = (".claude", "verification")
29
- ALL_VERDICT_PATH_SEGMENT_BODIES = ("claude", "verification")
30
- HEX_TOKEN_PATTERN = r"(?<![0-9a-fx])([0-9a-f]{6,})(?![0-9a-f])"
31
- BASE64_TOKEN_PATTERN = r"[A-Za-z0-9+/]{8,}={0,2}"
32
- CHARACTER_CODE_SEQUENCE_PATTERN = r"\d{1,3}(?:\s*,\s*\d{1,3})+"
33
- CHR_CALL_CHAIN_PATTERN = r"chr\(\s*\d{1,3}\s*\)(?:\s*\+\s*chr\(\s*\d{1,3}\s*\))+"
34
- CHR_CALL_CODE_PATTERN = r"chr\(\s*(\d{1,3})\s*\)"
35
- HEX_DIGITS_PER_BYTE = 2
36
- FILE_WRITE_PRIMITIVE_PATTERN = (
37
- r"\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
38
- r"|Out-File|Set-Content|Add-Content|\btee\b|>"
39
- )
40
- NON_REDIRECT_FILE_WRITE_PRIMITIVE_PATTERN = (
41
- r"\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
42
- r"|Out-File|Set-Content|Add-Content|\btee\b"
43
- )
44
- WRITE_CALL_REGION_PATTERN = (
45
- r"(?:\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
46
- r"|Out-File|Set-Content|Add-Content|\btee\b)[^;&|\n]*"
47
- )
48
- VERDICT_KEY_ALL_PASS = "all_pass"
49
- VERDICT_KEY_MANIFEST_SHA256 = "manifest_sha256"
50
- VERDICT_KEY_FINDINGS = "findings"
51
- VERDICT_FINDING_CHECK_KEY = "check"
52
- LOGS_DIRECTORY_NAME = "logs"
53
- VERIFIER_VERDICTS_JSONL_FILENAME = "verifier-verdicts.jsonl"
54
- LEDGER_KEY_TIMESTAMP = "timestamp"
55
- LEDGER_KEY_REPO_ROOT = "repo_root"
56
- LEDGER_KEY_BRANCH = "branch"
57
- LEDGER_KEY_MANIFEST_HASH = "manifest_hash"
58
- LEDGER_KEY_ALL_PASS = "all_pass"
59
- LEDGER_KEY_FINDING_COUNT = "finding_count"
60
- LEDGER_KEY_FINDING_CHECK_NAMES = "finding_check_names"
61
- LEDGER_KEY_MINTED_FROM_AGENT_ID = "minted_from_agent_id"
62
- VERDICT_FILE_GLOB = "*.json"
63
- SUBAGENTS_DIRECTORY_NAME = "subagents"
64
- AGENT_TRANSCRIPT_GLOB = "agent-*.jsonl"
65
- AGENT_META_SIDECAR_SUFFIX = ".meta.json"
66
- AGENT_META_TYPE_KEY = "agentType"
67
- TRANSCRIPT_ENTRY_TYPE_KEY = "type"
68
- TRANSCRIPT_ASSISTANT_ENTRY_TYPE = "assistant"
69
- TRANSCRIPT_MESSAGE_KEY = "message"
70
- TRANSCRIPT_CONTENT_KEY = "content"
71
- TRANSCRIPT_CONTENT_TYPE_KEY = "type"
72
- TRANSCRIPT_TEXT_CONTENT_TYPE = "text"
73
- TRANSCRIPT_TEXT_KEY = "text"
74
- VERDICT_FENCE_PATTERN = r"```verdict\s*\n(.*?)```"
75
- MANIFEST_HASH_CLI_FLAG = "--manifest-hash"
76
- MANIFEST_HASH_FOR_BRANCH_CLI_FLAG = "--manifest-hash-for-branch"
77
- WORKTREE_LIST_PATH_PREFIX = "worktree "
78
- WORKTREE_LIST_BRANCH_PREFIX = "branch "
79
- BRANCH_REFERENCE_PREFIX = "refs/heads/"
80
- EMPTY_SURFACE_GUARD_MESSAGE = (
81
- "ERROR: The work tree at {repo_root} has no changed or untracked files "
82
- "versus origin/main (empty change surface). This work tree holds nothing "
83
- "to verify — you are pointed at the wrong work tree. Run `git worktree list` "
84
- "to see all checked-out work trees and target the one on the branch under review."
85
- )
86
- BRANCH_WORKTREE_ABSENT_MESSAGE = (
87
- "ERROR: No work tree has branch '{branch}' checked out. "
88
- "Check it out in a work tree first: "
89
- "`git worktree add <path> <branch>` or `git checkout <branch>`."
90
- )
91
- DOCS_ONLY_EXTENSIONS = frozenset(
92
- {".md", ".txt", ".rst", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico"}
93
- )
94
- PYTHON_EXTENSION = ".py"
95
- TEST_FILE_PREFIX = "test_"
96
- TEST_FILE_SUFFIX = "_test.py"
97
- CONFTEST_FILE_NAME = "conftest.py"
98
- MINIMUM_STATUS_FIELD_COUNT = 2
99
- ALL_FALLBACK_BASE_REFERENCES = ("origin/main", "origin/master")
100
- DETACHED_HEAD_LABEL = "HEAD"
101
- BRANCH_REMOTE_CONFIG_KEY_TEMPLATE = "branch.{branch_name}.remote"
102
- ALL_TOOLING_STATE_PREFIXES = (
103
- ".claude/verification/",
104
- ".claude/worktrees/",
105
- ".claude/daemon/",
106
- ".claude/teams/",
107
- ".claude/sessions/",
108
- ".cursor/worktrees/",
109
- )
110
- GATED_GIT_SUBCOMMANDS = frozenset({"commit", "push"})
111
- ALL_GIT_BINARY_NAMES = frozenset({"git", "git.exe"})
112
- VALUE_TAKING_GIT_OPTIONS = frozenset({"-C", "-c", "--git-dir", "--work-tree", "--namespace"})
113
- REPO_DIRECTORY_OPTION = "-C"
114
- WORK_TREE_OPTION = "--work-tree"
115
- DIRECTORY_CHANGE_VERBS = frozenset({"cd", "pushd", "set-location", "sl"})
116
- DIRECTORY_CHANGE_PATH_OPTIONS = frozenset({"-path", "-literalpath"})
117
- DIRECTORY_CHANGE_OPTION_TERMINATOR = "--"
118
- DIRECTORY_CHANGE_PATTERN_PREFIX = r"(?:^|(?<=[\s;&|(]))(?:"
119
- DIRECTORY_CHANGE_PATTERN_SUFFIX = r")(?=\s|$)"
120
- DIRECTORY_CHANGE_OPTION_PREFIX_PATTERN = r"(?:[ \t]+(?:%s)(?=\s|$))*"
121
- DIRECTORY_CHANGE_TARGET_PATTERN = r"[ \t]+['\"]?\S*"
122
- CLAUDE_HOME_TARGET_BOUNDARY_PATTERN = r"[\\/]?['\"]?(?=[\s;&|]|$)"
123
- VERDICT_DIRECTORY_TARGET_BOUNDARY_PATTERN = r"[\\/]?['\"]?(?=[\s;&|]|$)"
124
- COMMAND_AFTER_DIRECTORY_CHANGE_PATTERN = r"[;&|\n][\s]*\S"
125
- OPTION_WITH_VALUE_STEP = 2
126
- ALL_GATED_TOOL_NAMES = ("Bash", "PowerShell")
127
- HASH_PREVIEW_LENGTH = 16
128
- VERIFICATION_BYPASS_MARKER = "# verify-skip"
129
- MINTING_AGENT_TYPE = "code-verifier"
130
- VERDICT_DIRECTORY_GUARD_MESSAGE = (
131
- "BLOCKED: [VERDICT_DIRECTORY_GUARD] Shell access to the verification "
132
- "verdict directory (~/.claude/verification/) is denied. Only the "
133
- "verifier_verdict_minter.py SubagentStop hook writes verdict files; a "
134
- "shell write here would forge a passing verdict and defeat the "
135
- "verified-commit gate. Spawn the code-verifier agent to earn a verdict "
136
- "instead of writing one."
137
- )
138
- CORRECTIVE_MESSAGE = (
139
- "BLOCKED: [VERIFIED_COMMIT_GATE] This branch surface has no passing "
140
- "verification verdict. Spawn the code-verifier agent (Agent tool, "
141
- "subagent_type 'code-verifier') with the task texts, the diff scope, "
142
- "and recorded baselines; when it finishes with a clean verdict the "
143
- "SubagentStop hook mints the verdict and this command will pass. Any "
144
- "file change after verification invalidates the verdict, so verify "
145
- "last. The verdict binds to the exact surface content (every changed "
146
- "and untracked file's bytes), not to a work-tree path, so a commit "
147
- "from a different work tree passes only when its surface is "
148
- "byte-identical; a work tree carrying any extra or later edit has a "
149
- "surface no verdict covers. To land this change, run this command "
150
- "from the work tree that holds the verified surface, or verify this "
151
- "surface here. Exempt automatically: docs/image files, pytest test "
152
- "files, and "
153
- "Python files whose docstring- and comment-stripped AST is unchanged "
154
- "(comment-only edits in non-Python files are not exempt)."
155
- )
1
+ """Constants for the verified-commit gate hook family.
2
+
3
+ Shared by ``verification_verdict_store.py``, ``verified_commit_gate.py``,
4
+ and ``verifier_verdict_minter.py`` so every tunable lives in one place.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ GIT_TIMEOUT_SECONDS = 30
10
+ ROOT_KEY_HEX_LENGTH = 16
11
+ VERDICT_JSON_INDENT = 2
12
+ CLAUDE_HOME_DIRECTORY_NAME = ".claude"
13
+ VERDICT_DIRECTORY_NAME = "verification"
14
+ VERDICT_DIRECTORY_NAME_SEPARATOR_PATTERN = r"['\"\\/,\s]+"
15
+ VERDICT_DIRECTORY_PATH_BOUNDARY_PATTERN = r"(?=['\"]*[\\/,])"
16
+ RELATIVE_VERDICT_DIRECTORY_PATTERN = r"(?:^|(?<=[\s;&|(='\"]))verification[\\/]"
17
+ VERDICT_PATH_GLUE_PATTERN = r"['\"+\\/\s]*[\\/]['\"+\\/\s]*"
18
+ VERDICT_DIRECTORY_CHANGE_TARGET_PATTERN = r"[ \t]+['\"]?verification[\\/]?['\"]?(?=[\s;&|]|$)"
19
+ VERDICT_PATH_JOINED_VARIABLE_PATTERN = r"\$\{?(\w+)\}?[\\/]|[\\/]\$\{?(\w+)\}?"
20
+ VERDICT_PATH_VARIABLE_ASSIGNMENT_PATTERN = r"(?:^|(?<=[\s;&|(]))%s=(\S+)"
21
+ VERDICT_FILE_RELATIVE_REFERENCE_PATTERN = (
22
+ rf"(?:^|(?<=[\s;&|(='\"\\/]))verification[\\/][0-9a-f]{{{ROOT_KEY_HEX_LENGTH}}}\.json"
23
+ )
24
+ PATH_OBFUSCATION_PRIMITIVE_PATTERN = (
25
+ r"chr\s*\(|bytes\.fromhex\s*\(|b64decode\s*\(|codecs\.decode\s*\("
26
+ r"|(?:bytes|bytearray)\s*\(\s*\[|\[char\[?\]?\]"
27
+ )
28
+ ALL_VERDICT_PATH_SEGMENT_NAMES = (".claude", "verification")
29
+ ALL_VERDICT_PATH_SEGMENT_BODIES = ("claude", "verification")
30
+ HEX_TOKEN_PATTERN = r"(?<![0-9a-fx])([0-9a-f]{6,})(?![0-9a-f])"
31
+ BASE64_TOKEN_PATTERN = r"[A-Za-z0-9+/]{8,}={0,2}"
32
+ CHARACTER_CODE_SEQUENCE_PATTERN = r"\d{1,3}(?:\s*,\s*\d{1,3})+"
33
+ CHR_CALL_CHAIN_PATTERN = r"chr\(\s*\d{1,3}\s*\)(?:\s*\+\s*chr\(\s*\d{1,3}\s*\))+"
34
+ CHR_CALL_CODE_PATTERN = r"chr\(\s*(\d{1,3})\s*\)"
35
+ HEX_DIGITS_PER_BYTE = 2
36
+ FILE_WRITE_PRIMITIVE_PATTERN = (
37
+ r"\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
38
+ r"|Out-File|Set-Content|Add-Content|\btee\b|>"
39
+ )
40
+ NON_REDIRECT_FILE_WRITE_PRIMITIVE_PATTERN = (
41
+ r"\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
42
+ r"|Out-File|Set-Content|Add-Content|\btee\b"
43
+ )
44
+ WRITE_CALL_REGION_PATTERN = (
45
+ r"(?:\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
46
+ r"|Out-File|Set-Content|Add-Content|\btee\b)[^;&|\n]*"
47
+ )
48
+ VERDICT_KEY_ALL_PASS = "all_pass"
49
+ VERDICT_KEY_MANIFEST_SHA256 = "manifest_sha256"
50
+ VERDICT_KEY_FINDINGS = "findings"
51
+ VERDICT_FINDING_CHECK_KEY = "check"
52
+ LOGS_DIRECTORY_NAME = "logs"
53
+ VERIFIER_VERDICTS_JSONL_FILENAME = "verifier-verdicts.jsonl"
54
+ LEDGER_KEY_TIMESTAMP = "timestamp"
55
+ LEDGER_KEY_REPO_ROOT = "repo_root"
56
+ LEDGER_KEY_BRANCH = "branch"
57
+ LEDGER_KEY_MANIFEST_HASH = "manifest_hash"
58
+ LEDGER_KEY_ALL_PASS = "all_pass"
59
+ LEDGER_KEY_FINDING_COUNT = "finding_count"
60
+ LEDGER_KEY_FINDING_CHECK_NAMES = "finding_check_names"
61
+ LEDGER_KEY_MINTED_FROM_AGENT_ID = "minted_from_agent_id"
62
+ VERDICT_FILE_GLOB = "*.json"
63
+ SUBAGENTS_DIRECTORY_NAME = "subagents"
64
+ AGENT_TRANSCRIPT_GLOB = "agent-*.jsonl"
65
+ AGENT_META_SIDECAR_SUFFIX = ".meta.json"
66
+ AGENT_META_TYPE_KEY = "agentType"
67
+ TRANSCRIPT_ENTRY_TYPE_KEY = "type"
68
+ TRANSCRIPT_ASSISTANT_ENTRY_TYPE = "assistant"
69
+ TRANSCRIPT_MESSAGE_KEY = "message"
70
+ TRANSCRIPT_CONTENT_KEY = "content"
71
+ TRANSCRIPT_CONTENT_TYPE_KEY = "type"
72
+ TRANSCRIPT_TEXT_CONTENT_TYPE = "text"
73
+ TRANSCRIPT_TEXT_KEY = "text"
74
+ VERDICT_FENCE_PATTERN = r"```verdict\s*\n(.*?)```"
75
+ MANIFEST_HASH_CLI_FLAG = "--manifest-hash"
76
+ MANIFEST_HASH_FOR_BRANCH_CLI_FLAG = "--manifest-hash-for-branch"
77
+ WORKTREE_LIST_PATH_PREFIX = "worktree "
78
+ WORKTREE_LIST_BRANCH_PREFIX = "branch "
79
+ BRANCH_REFERENCE_PREFIX = "refs/heads/"
80
+ EMPTY_SURFACE_GUARD_MESSAGE = (
81
+ "ERROR: The work tree at {repo_root} has no changed or untracked files "
82
+ "versus origin/main (empty change surface). This work tree holds nothing "
83
+ "to verify — you are pointed at the wrong work tree. Run `git worktree list` "
84
+ "to see all checked-out work trees and target the one on the branch under review."
85
+ )
86
+ BRANCH_WORKTREE_ABSENT_MESSAGE = (
87
+ "ERROR: No work tree has branch '{branch}' checked out. "
88
+ "Check it out in a work tree first: "
89
+ "`git worktree add <path> <branch>` or `git checkout <branch>`."
90
+ )
91
+ DOCS_ONLY_EXTENSIONS = frozenset(
92
+ {".md", ".txt", ".rst", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico"}
93
+ )
94
+ PYTHON_EXTENSION = ".py"
95
+ TEST_FILE_PREFIX = "test_"
96
+ TEST_FILE_SUFFIX = "_test.py"
97
+ CONFTEST_FILE_NAME = "conftest.py"
98
+ MINIMUM_STATUS_FIELD_COUNT = 2
99
+ ALL_FALLBACK_BASE_REFERENCES = ("origin/main", "origin/master")
100
+ DETACHED_HEAD_LABEL = "HEAD"
101
+ BRANCH_REMOTE_CONFIG_KEY_TEMPLATE = "branch.{branch_name}.remote"
102
+ ALL_TOOLING_STATE_PREFIXES = (
103
+ ".claude/verification/",
104
+ ".claude/worktrees/",
105
+ ".claude/daemon/",
106
+ ".claude/teams/",
107
+ ".claude/sessions/",
108
+ ".cursor/worktrees/",
109
+ ".claude-plugin-data/",
110
+ )
111
+ GATED_GIT_SUBCOMMANDS = frozenset({"commit", "push"})
112
+ ALL_GIT_BINARY_NAMES = frozenset({"git", "git.exe"})
113
+ VALUE_TAKING_GIT_OPTIONS = frozenset({"-C", "-c", "--git-dir", "--work-tree", "--namespace"})
114
+ REPO_DIRECTORY_OPTION = "-C"
115
+ WORK_TREE_OPTION = "--work-tree"
116
+ DIRECTORY_CHANGE_VERBS = frozenset({"cd", "pushd", "set-location", "sl"})
117
+ DIRECTORY_CHANGE_PATH_OPTIONS = frozenset({"-path", "-literalpath"})
118
+ DIRECTORY_CHANGE_OPTION_TERMINATOR = "--"
119
+ DIRECTORY_CHANGE_PATTERN_PREFIX = r"(?:^|(?<=[\s;&|(]))(?:"
120
+ DIRECTORY_CHANGE_PATTERN_SUFFIX = r")(?=\s|$)"
121
+ DIRECTORY_CHANGE_OPTION_PREFIX_PATTERN = r"(?:[ \t]+(?:%s)(?=\s|$))*"
122
+ DIRECTORY_CHANGE_TARGET_PATTERN = r"[ \t]+['\"]?\S*"
123
+ CLAUDE_HOME_TARGET_BOUNDARY_PATTERN = r"[\\/]?['\"]?(?=[\s;&|]|$)"
124
+ VERDICT_DIRECTORY_TARGET_BOUNDARY_PATTERN = r"[\\/]?['\"]?(?=[\s;&|]|$)"
125
+ COMMAND_AFTER_DIRECTORY_CHANGE_PATTERN = r"[;&|\n][\s]*\S"
126
+ OPTION_WITH_VALUE_STEP = 2
127
+ ALL_GATED_TOOL_NAMES = ("Bash", "PowerShell")
128
+ HASH_PREVIEW_LENGTH = 16
129
+ VERIFICATION_BYPASS_MARKER = "# verify-skip"
130
+ MINTING_AGENT_TYPE = "code-verifier"
131
+ VERDICT_DIRECTORY_GUARD_MESSAGE = (
132
+ "BLOCKED: [VERDICT_DIRECTORY_GUARD] Shell access to the verification "
133
+ "verdict directory (~/.claude/verification/) is denied. Only the "
134
+ "verifier_verdict_minter.py SubagentStop hook writes verdict files; a "
135
+ "shell write here would forge a passing verdict and defeat the "
136
+ "verified-commit gate. Spawn the code-verifier agent to earn a verdict "
137
+ "instead of writing one."
138
+ )
139
+ CORRECTIVE_MESSAGE = (
140
+ "BLOCKED: [VERIFIED_COMMIT_GATE] This branch surface has no passing "
141
+ "verification verdict. Clear commit-committability gates first "
142
+ "(CODE_RULES on changed lines and merge conflicts vs base the "
143
+ "code_verifier_spawn_preflight_gate enforces these on the spawn). If a "
144
+ "prior commit failed on CODE_RULES or staging, fix those before "
145
+ "re-verifying. Then spawn the code-verifier agent (Agent or Task tool, "
146
+ "subagent_type 'code-verifier') with the task texts, the diff scope, "
147
+ "and recorded baselines; when it finishes with a clean verdict the "
148
+ "SubagentStop hook mints the verdict and this command will pass. Any "
149
+ "file change after verification invalidates the verdict, so verify "
150
+ "last only after commit gates are green. The verdict binds to the exact "
151
+ "surface content (every changed and untracked file's bytes), not to a "
152
+ "work-tree path, so a commit from a different work tree passes only "
153
+ "when its surface is byte-identical; a work tree carrying any extra or "
154
+ "later edit has a surface no verdict covers. To land this change, run "
155
+ "this command from the work tree that holds the verified surface, or "
156
+ "verify this surface here. Exempt automatically: docs/image files, "
157
+ "pytest test files, and "
158
+ "Python files whose docstring- and comment-stripped AST is unchanged "
159
+ "(comment-only edits in non-Python files are not exempt)."
160
+ )
@@ -26,6 +26,8 @@ _MIRRORED_BLOCKING_FILE_NAMES = (
26
26
  "verifier_verdict_minter.py",
27
27
  "verdict_directory_write_blocker.py",
28
28
  "verified_commit_config_bootstrap.py",
29
+ "code_review_enforcement_config_bootstrap.py",
30
+ "code_review_stamp_store.py",
29
31
  )
30
32
  _DECOY_CONFIG_INIT_SOURCE = '"""Foreign config package that must never win resolution."""\n'
31
33
  _DECOY_CONFIG_CONSTANTS_SOURCE = (
@@ -18,11 +18,19 @@ _hooks_dir = str(Path(__file__).resolve().parent.parent)
18
18
  if _hooks_dir not in sys.path:
19
19
  sys.path.insert(0, _hooks_dir)
20
20
 
21
+ from blocking.pr_description_pr_number import ( # noqa: E402
22
+ _extract_pr_number_from_command,
23
+ )
21
24
  from hooks_constants.convergence_gate_blocker_constants import ( # noqa: E402
25
+ ALL_GH_PR_VIEW_NUMBER_COMMAND,
26
+ BASH_LINE_CONTINUATION_PATTERN,
22
27
  COMMAND_SEPARATOR_PATTERN,
23
28
  GH_PR_READY_ANCHOR_PATTERN,
29
+ GH_REPO_FLAG,
30
+ GIT_URL_SUFFIX,
24
31
  PR_URL_OWNER_REPO_NUMBER_PATTERN,
25
32
  REPO_OVERRIDE_FLAG_PATTERN,
33
+ REPO_SLUG_TEMPLATE,
26
34
  )
27
35
  from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
28
36
 
@@ -36,12 +44,15 @@ def _ready_command_segment(command: str) -> str:
36
44
  ^^^^^^^^^^^^^^^^ clipped -> returned segment
37
45
 
38
46
  Scanning only this segment keeps a ``--repo`` flag or PR URL that belongs to
39
- a chained command from binding the gate to the wrong PR.
47
+ a chained command from binding the gate to the wrong PR. A backslash-newline
48
+ continuation is folded to a space before the separator search, so a
49
+ ``--repo`` flag written on a continued line stays inside the segment.
40
50
  """
41
51
  ready_match = re.search(GH_PR_READY_ANCHOR_PATTERN, command)
42
52
  if ready_match is None:
43
53
  return command
44
54
  ready_tail = command[ready_match.start() :]
55
+ ready_tail = re.sub(BASH_LINE_CONTINUATION_PATTERN, " ", ready_tail)
45
56
  separator_match = re.search(COMMAND_SEPARATOR_PATTERN, ready_tail)
46
57
  if separator_match is None:
47
58
  return ready_tail
@@ -61,30 +72,59 @@ def _parse_repo_flag(command: str) -> tuple[str, str] | None:
61
72
  flag_match = re.search(REPO_OVERRIDE_FLAG_PATTERN, command)
62
73
  if flag_match is None:
63
74
  return None
64
- return flag_match.group("owner"), flag_match.group("repo")
75
+ return flag_match.group("owner"), flag_match.group("repo").removesuffix(GIT_URL_SUFFIX)
76
+
77
+
78
+ def _resolve_named_identity(
79
+ command: str, cwd: str | None
80
+ ) -> tuple[tuple[str, str] | None, int] | None:
81
+ """Resolve the repository the command names and its PR number.
82
+
83
+ A full PR URL yields both the (owner, repo) pair and the number. A
84
+ ``--repo``/``-R`` flag yields the pair while the number resolves from the
85
+ command's positional argument or the named repository's current-branch PR.
86
+ With neither present, the pair is None — leaving gh bound to the current
87
+ directory's repository — and the number resolves the same way. Every parse
88
+ runs over the ``gh pr ready`` segment alone, clipped at the next command
89
+ separator, so a flag or URL belonging to a chained command cannot bind the
90
+ gate to the wrong PR.
91
+
92
+ Args:
93
+ command: The raw shell command captured by the hook.
94
+ cwd: The working directory gh runs in when the number resolves from the
95
+ current branch, or None for the process default.
96
+
97
+ Returns:
98
+ The (owner, repo) pair the command names — None when it names none —
99
+ paired with the resolved PR number, or None when no number resolves.
100
+ """
101
+ ready_segment = _ready_command_segment(command)
102
+ pr_url_identity = _parse_pr_url(ready_segment)
103
+ if pr_url_identity is not None:
104
+ url_owner, url_repo, url_number = pr_url_identity
105
+ return (url_owner, url_repo), url_number
106
+ all_target_repo = _parse_repo_flag(ready_segment)
107
+ from_owner, from_repo = all_target_repo if all_target_repo is not None else (None, None)
108
+ resolved_pr_number = _resolve_pr_number(ready_segment, cwd, from_owner, from_repo)
109
+ if resolved_pr_number is None:
110
+ return None
111
+ return all_target_repo, resolved_pr_number
65
112
 
66
113
 
67
114
  def _resolve_target_identity(command: str, cwd: str | None) -> tuple[str, str, int] | None:
68
115
  """Resolve the (owner, repo, pr_number) the gate keys its evidence to.
69
116
 
70
- A full PR URL in the command yields all three. A --repo/-R flag yields
71
- the repository while the PR number resolves from the command number or
72
- the cwd. With neither present, both the repository and the number
73
- resolve from the cwd.
117
+ Delegates the repository-and-number resolution to the shared core, then
118
+ falls back to the cwd repository for the number when the command names no
119
+ repository of its own.
74
120
  """
75
- ready_segment = _ready_command_segment(command)
76
- pr_url_identity = _parse_pr_url(ready_segment)
77
- if pr_url_identity is not None:
78
- return pr_url_identity
79
-
80
- pr_number = _resolve_pr_number(ready_segment, cwd)
81
- if pr_number is None:
121
+ named_identity = _resolve_named_identity(command, cwd)
122
+ if named_identity is None:
82
123
  return None
83
-
84
- repo_flag_identity = _parse_repo_flag(ready_segment)
85
- if repo_flag_identity is not None:
86
- flag_owner, flag_repo = repo_flag_identity
87
- return flag_owner, flag_repo, pr_number
124
+ all_target_repo, pr_number = named_identity
125
+ if all_target_repo is not None:
126
+ owner, repo = all_target_repo
127
+ return owner, repo, pr_number
88
128
 
89
129
  cwd_identity = _resolve_owner_repo(cwd)
90
130
  if cwd_identity is None:
@@ -93,13 +133,46 @@ def _resolve_target_identity(command: str, cwd: str | None) -> tuple[str, str, i
93
133
  return cwd_owner, cwd_repo, pr_number
94
134
 
95
135
 
96
- def _resolve_pr_number(command: str, cwd: str | None) -> int | None:
97
- direct_match = re.search(r"\bgh\s+pr\s+ready\s+(\d+)", command)
98
- if direct_match:
99
- return int(direct_match.group(1))
136
+ def _repo_flag_arguments(all_target_repo: tuple[str, str] | None) -> list[str]:
137
+ """Build the ``--repo owner/repo`` arguments for a gh subcommand.
138
+
139
+ Args:
140
+ all_target_repo: The (owner, repo) pair the command names, or None when
141
+ the command names no repository.
142
+
143
+ Returns:
144
+ A ``[--repo, owner/repo]`` argument pair when a repository is named, or
145
+ an empty list when it is None — leaving gh bound to the current
146
+ directory's repository.
147
+ """
148
+ if all_target_repo is None:
149
+ return []
150
+ owner, repo = all_target_repo
151
+ return [GH_REPO_FLAG, REPO_SLUG_TEMPLATE.format(owner=owner, repo=repo)]
152
+
153
+
154
+ def _resolve_current_branch_pr_number(
155
+ all_target_repo: tuple[str, str] | None, cwd: str | None
156
+ ) -> int | None:
157
+ """Resolve the current-branch PR number through ``gh pr view``.
158
+
159
+ Args:
160
+ all_target_repo: The (owner, repo) pair the command names, or None to
161
+ read the current directory's repository.
162
+ cwd: The working directory gh runs in, or None for the process default.
163
+
164
+ Returns:
165
+ The PR number gh reports, or None when gh is missing, exits non-zero,
166
+ or prints no integer — every failure path returns None so the caller
167
+ fails open.
168
+ """
169
+ all_view_arguments = [
170
+ *ALL_GH_PR_VIEW_NUMBER_COMMAND,
171
+ *_repo_flag_arguments(all_target_repo),
172
+ ]
100
173
  try:
101
174
  completed_process = subprocess.run(
102
- ["gh", "pr", "view", "--json", "number", "--jq", ".number"],
175
+ all_view_arguments,
103
176
  capture_output=True,
104
177
  text=True,
105
178
  cwd=cwd or None,
@@ -115,6 +188,22 @@ def _resolve_pr_number(command: str, cwd: str | None) -> int | None:
115
188
  return None
116
189
 
117
190
 
191
+ def _resolve_pr_number(
192
+ command: str,
193
+ cwd: str | None,
194
+ from_owner: str | None,
195
+ from_repo: str | None,
196
+ ) -> int | None:
197
+ direct_number = _extract_pr_number_from_command(command)
198
+ if direct_number is not None:
199
+ return direct_number
200
+ if from_owner is not None and from_repo is not None:
201
+ all_target_repo: tuple[str, str] | None = (from_owner, from_repo)
202
+ else:
203
+ all_target_repo = None
204
+ return _resolve_current_branch_pr_number(all_target_repo, cwd)
205
+
206
+
118
207
  def _resolve_owner_repo(cwd: str | None) -> tuple[str, str] | None:
119
208
  try:
120
209
  completed_process = subprocess.run(
@@ -24,6 +24,8 @@ from hooks_constants.destructive_command_segment_constants import ( # noqa: E40
24
24
  ALL_BENIGN_COMPOUND_SEGMENT_COMMANDS,
25
25
  ALL_COMMAND_LAUNCHER_WRAPPER_COMMANDS,
26
26
  ALL_KNOWN_TEMPORARY_ENVIRONMENT_VARIABLE_NAMES,
27
+ ALL_TRUTHY_ENV_VALUES,
28
+ DESTRUCTIVE_DENY_MODE_ENV_VAR,
27
29
  ALL_FILE_WRITING_OUTPUT_FLAGS_BY_BENIGN_PROGRAM,
28
30
  ALL_FIND_EXEC_ACTION_FLAGS,
29
31
  ALL_FIND_EXEC_ACTION_TERMINATORS,
@@ -56,12 +58,17 @@ from hooks_constants.destructive_command_segment_constants import ( # noqa: E40
56
58
 
57
59
  CLAUDE_DIRECTORY_PATH = os.path.normpath(os.path.expanduser("~/.claude"))
58
60
  GH_REDIRECT_ACTIVE_ENV_VAR = "CLAUDE_GH_REDIRECT_ACTIVE"
59
- GH_REDIRECT_ACTIVE_TRUTHY_VALUES = frozenset({"1", "true", "yes", "on"})
60
61
 
61
62
 
62
63
  def gh_redirect_is_active() -> bool:
63
64
  env_var_value = os.environ.get(GH_REDIRECT_ACTIVE_ENV_VAR, "").strip().lower()
64
- return env_var_value in GH_REDIRECT_ACTIVE_TRUTHY_VALUES
65
+ return env_var_value in ALL_TRUTHY_ENV_VALUES
66
+
67
+
68
+ def deny_mode_is_active() -> bool:
69
+ env_var_value = os.environ.get(DESTRUCTIVE_DENY_MODE_ENV_VAR, "").strip().lower()
70
+ return env_var_value in ALL_TRUTHY_ENV_VALUES
71
+
65
72
 
66
73
  def directory_is_ephemeral(directory_path: str) -> bool:
67
74
  """Return True when a directory belongs to the ephemeral auto-allow namespace.
@@ -2043,14 +2050,20 @@ def main() -> None:
2043
2050
  matched_description = co_resident_hazard_description
2044
2051
 
2045
2052
  if matched_description is not None:
2046
- ask_response = {
2053
+ if deny_mode_is_active():
2054
+ terminal_decision = "deny"
2055
+ terminal_reason = f"DESTRUCTIVE: {matched_description}. Blocked in deny mode."
2056
+ else:
2057
+ terminal_decision = "ask"
2058
+ terminal_reason = f"DESTRUCTIVE: {matched_description}. Requires explicit user approval."
2059
+ terminal_response = {
2047
2060
  "hookSpecificOutput": {
2048
2061
  "hookEventName": "PreToolUse",
2049
- "permissionDecision": "ask",
2050
- "permissionDecisionReason": f"DESTRUCTIVE: {matched_description}. Requires explicit user approval."
2062
+ "permissionDecision": terminal_decision,
2063
+ "permissionDecisionReason": terminal_reason
2051
2064
  }
2052
2065
  }
2053
- print(json.dumps(ask_response))
2066
+ print(json.dumps(terminal_response))
2054
2067
 
2055
2068
  sys.exit(0)
2056
2069