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
@@ -0,0 +1,38 @@
1
+ """Behavioral tests for the split directory-change-into-stamp matcher.
2
+
3
+ The matcher blocks a command that changes into the Claude home, then into a
4
+ relative ``code-review-stamps`` directory, then runs any further command, while
5
+ an unrelated second change or a benign command passes.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from code_review_stamp_write_blocker_parts.split_directory_change_into_stamp import (
11
+ changes_through_split_directory_into_stamp,
12
+ )
13
+
14
+ SPLIT_CHANGE_FORGE = "cd ~/.claude && cd code-review-stamps && echo x > f.json"
15
+ UNRELATED_SECOND_CHANGE = "cd ~/.claude && cd hooks && echo x > f.json"
16
+ PYTEST_RUN = "python -m pytest test_code_review_stamp_store.py"
17
+
18
+
19
+ def test_split_change_into_stamp_then_command_is_blocked() -> None:
20
+ assert changes_through_split_directory_into_stamp(SPLIT_CHANGE_FORGE)
21
+
22
+
23
+ def test_unrelated_second_change_is_not_blocked() -> None:
24
+ assert not changes_through_split_directory_into_stamp(UNRELATED_SECOND_CHANGE)
25
+
26
+
27
+ def test_pytest_run_is_not_blocked() -> None:
28
+ assert not changes_through_split_directory_into_stamp(PYTEST_RUN)
29
+
30
+
31
+ def test_change_into_stamp_without_following_command_is_not_blocked() -> None:
32
+ assert not changes_through_split_directory_into_stamp("cd ~/.claude && cd code-review-stamps")
33
+
34
+
35
+ def test_pushd_split_change_into_stamp_then_command_is_blocked() -> None:
36
+ assert changes_through_split_directory_into_stamp(
37
+ "pushd ~/.claude && pushd code-review-stamps && cp a b"
38
+ )
@@ -1,18 +1,19 @@
1
1
  #!/usr/bin/env python3
2
2
  """PreToolUse hook: pre-flight gate for the code-verifier subagent spawn.
3
3
 
4
- The hook fires only on an ``Agent`` tool call whose ``subagent_type`` is
4
+ The hook fires on an ``Agent`` or ``Task`` tool call whose ``subagent_type`` is
5
5
  ``code-verifier``. Before that verification spawn runs, the hook checks the
6
6
  branch for two committability problems against the resolved base ref: a real
7
7
  merge conflict (a non-mutating trial-merge of HEAD against the base ref) and a
8
8
  CODE_RULES violation on a line added in the working tree since the merge base
9
- (committed on the branch or uncommitted). When
10
- either fires, the hook denies the spawn with a reason addressed to the spawning
11
- agent that names the conflicting files and the violating file:line, so that
12
- agent fixes them and re-spawns. Both checks fail OPEN on any infrastructure
13
- problem — a non-repo cwd, an absent base ref, a git or engine failure, or a
14
- timeout because the authoritative fail-closed CODE_RULES enforcement already
15
- runs at Write time and at commit time. The hook never network-fetches and never
9
+ (committed on the branch or uncommitted). When either fires, the hook denies
10
+ the spawn with a reason addressed to the spawning agent that names the
11
+ conflicting files and the violating file:line, so that agent fixes them and
12
+ re-spawns. A CODE_RULES engine import/load failure is fail-closed (deny with a
13
+ named load-failure section). Environmental problems — a non-repo cwd, an
14
+ absent base ref, a git failure, an unreadable file alone, or a timeout — fail
15
+ OPEN because the authoritative fail-closed CODE_RULES enforcement already runs
16
+ at Write time and at commit time. The hook never network-fetches and never
16
17
  mutates the index or working tree.
17
18
  """
18
19
 
@@ -21,6 +22,7 @@ from __future__ import annotations
21
22
  import contextlib
22
23
  import io
23
24
  import json
25
+ import re
24
26
  import subprocess
25
27
  import sys
26
28
  from pathlib import Path
@@ -43,6 +45,7 @@ from verification_verdict_store import ( # noqa: E402
43
45
  )
44
46
 
45
47
  from hooks_constants.code_verifier_spawn_preflight_gate_constants import ( # noqa: E402
48
+ ALL_CODE_VERIFIER_SPAWN_TOOL_NAMES,
46
49
  ALL_MERGE_HEAD_PROBE_FLAGS,
47
50
  ALL_MERGE_TREE_COMMAND_FLAGS,
48
51
  ALL_NAME_ONLY_WORKTREE_DIFF_FLAGS,
@@ -51,6 +54,7 @@ from hooks_constants.code_verifier_spawn_preflight_gate_constants import ( # no
51
54
  CODE_RULES_SECTION_HEADER,
52
55
  CODE_VERIFIER_SUBAGENT_TYPE,
53
56
  DENY_REASON_LEAD,
57
+ ENGINE_LOAD_FAILURE_SECTION,
54
58
  GATE_SCRIPTS_RELATIVE_PATH,
55
59
  MERGE_CONFLICT_SECTION_HEADER,
56
60
  MERGE_TREE_CLEAN_EXIT_CODE,
@@ -58,9 +62,6 @@ from hooks_constants.code_verifier_spawn_preflight_gate_constants import ( # no
58
62
  MERGE_TREE_TIMEOUT_SECONDS,
59
63
  )
60
64
  from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
61
- from hooks_constants.pr_converge_bugteam_enforcer_constants import ( # noqa: E402
62
- AGENT_TOOL_NAME,
63
- )
64
65
 
65
66
  _scripts_dir = str(Path(__file__).resolve().parents[2] / GATE_SCRIPTS_RELATIVE_PATH)
66
67
  if _scripts_dir not in sys.path:
@@ -78,17 +79,17 @@ from code_rules_gate import ( # noqa: E402
78
79
 
79
80
 
80
81
  def _should_run(payload_by_field: dict[str, object]) -> bool:
81
- """Return True only for a code-verifier Agent spawn.
82
+ """Return True only for a code-verifier Agent or Task spawn.
82
83
 
83
84
  Args:
84
85
  payload_by_field: The full PreToolUse hook payload (already
85
86
  JSON-parsed), keyed by top-level field name.
86
87
 
87
88
  Returns:
88
- True when the tool is Agent and ``tool_input.subagent_type`` is
89
- ``code-verifier``; False for every other shape.
89
+ True when the tool is Agent or Task and ``tool_input.subagent_type``
90
+ is ``code-verifier``; False for every other shape.
90
91
  """
91
- if payload_by_field.get("tool_name", "") != AGENT_TOOL_NAME:
92
+ if payload_by_field.get("tool_name", "") not in ALL_CODE_VERIFIER_SPAWN_TOOL_NAMES:
92
93
  return False
93
94
  tool_input = payload_by_field.get("tool_input", {})
94
95
  if not isinstance(tool_input, dict):
@@ -297,7 +298,7 @@ def _tracked_file_added_lines(repo_root: str, merge_base_sha: str, relative_path
297
298
  def _code_rules_report(
298
299
  repo_root: str, all_file_paths: list[Path], all_added_lines_by_path: dict[Path, set[int]]
299
300
  ) -> str | None:
300
- """Run the CODE_RULES engine and return its blocking report, or None.
301
+ """Run the CODE_RULES engine and return a full deny section, or None.
301
302
 
302
303
  Args:
303
304
  repo_root: The repository top-level directory.
@@ -306,19 +307,30 @@ def _code_rules_report(
306
307
  by resolved absolute path.
307
308
 
308
309
  Returns:
309
- The engine's grouped file:line report when a blocking violation lands
310
- on an added line, or None when the surface is clean, only an unreadable
311
- changed file caused a non-zero gate exit, the engine fails to load, or
312
- any engine error arises — every non-block outcome fails OPEN. The
313
- harness hook timeout in hooks.json is the wall-clock bound on a runaway
314
- engine.
310
+ A full deny section when a blocking violation lands on an added line or
311
+ the CODE_RULES engine fails to import/load (fail-closed). None when the
312
+ surface is clean, only an unreadable changed file caused a non-zero gate
313
+ exit, or an environmental engine error arises — those non-block outcomes
314
+ fail OPEN. The harness hook timeout in hooks.json is the wall-clock
315
+ bound on a runaway engine.
315
316
  """
316
317
  if not all_file_paths:
317
318
  return None
318
319
  try:
319
320
  validate_content = load_validate_content()
320
321
  except SystemExit:
321
- return None
322
+ return ENGINE_LOAD_FAILURE_SECTION
323
+ except (
324
+ ImportError,
325
+ SyntaxError,
326
+ AttributeError,
327
+ RuntimeError,
328
+ TypeError,
329
+ NameError,
330
+ OSError,
331
+ re.error,
332
+ ):
333
+ return ENGINE_LOAD_FAILURE_SECTION
322
334
  try:
323
335
  blocking_present, captured_report = _run_gate_capturing_stderr(
324
336
  validate_content, all_file_paths, Path(repo_root), all_added_lines_by_path
@@ -327,7 +339,7 @@ def _code_rules_report(
327
339
  return None
328
340
  if not blocking_present:
329
341
  return None
330
- return captured_report
342
+ return f"{CODE_RULES_SECTION_HEADER}\n{captured_report.strip()}"
331
343
 
332
344
 
333
345
  def _run_gate_capturing_stderr(
@@ -382,8 +394,8 @@ def _build_deny_reason(
382
394
  all_conflicting_files: The conflicting file paths from the conflict
383
395
  check, an empty list when clean, or None when that check failed open.
384
396
  base_ref: The base ref named in the conflict section header.
385
- code_rules_report: The grouped report from the CODE_RULES check, or None
386
- when that check found nothing or failed open.
397
+ code_rules_report: A full CODE_RULES deny section (violations or engine
398
+ load failure), or None when that check found nothing or failed open.
387
399
 
388
400
  Returns:
389
401
  The full deny reason when either check fired, or None when neither
@@ -395,7 +407,7 @@ def _build_deny_reason(
395
407
  conflict_header = MERGE_CONFLICT_SECTION_HEADER.format(base_ref=base_ref)
396
408
  reason_sections.append(f"{conflict_header}\n{conflict_lines}")
397
409
  if code_rules_report:
398
- reason_sections.append(f"{CODE_RULES_SECTION_HEADER}\n{code_rules_report.strip()}")
410
+ reason_sections.append(code_rules_report.strip())
399
411
  if not reason_sections:
400
412
  return None
401
413
  return DENY_REASON_LEAD + "\n\n" + "\n\n".join(reason_sections)
@@ -1,5 +1,5 @@
1
- """Configuration package for the blocking hooks.
2
-
3
- A regular package (not a namespace package) so it resolves ahead of any
4
- same-named package later on ``sys.path``.
5
- """
1
+ """Configuration package for the blocking hooks.
2
+
3
+ A regular package (not a namespace package) so it resolves ahead of any
4
+ same-named package later on ``sys.path``.
5
+ """
@@ -0,0 +1,113 @@
1
+ """Single source of truth for the code-review enforcement gate family.
2
+
3
+ Holds the stamp directory name, the ordered effort tokens (``low`` under
4
+ ``medium`` under ``high`` under ``xhigh`` under ``max``, with ``ultra`` kept
5
+ out because it needs an interactive terminal), the effort a push and a
6
+ pull-request creation each require, the stamp record keys, the gate and
7
+ write-blocker messages, the store-forge shell patterns, the MCP create-PR
8
+ tool name, and the effort comparison every gate and the stamp store share so
9
+ the thresholds never drift between them, and the master enable flag
10
+ (``CODE_REVIEW_ENFORCEMENT_ENABLED``, default off) that every gate and
11
+ the stamp-directory write-blocker read before they enforce anything.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ CODE_REVIEW_ENFORCEMENT_ENABLED = False
17
+ STAMP_DIRECTORY_NAME = "code-review-stamps"
18
+ ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER = ("low", "medium", "high", "xhigh", "max")
19
+ PUSH_REQUIRED_EFFORT = "low"
20
+ PR_CREATE_REQUIRED_EFFORT = "xhigh"
21
+ STAMP_KEY_EFFORT = "effort"
22
+ STAMP_KEY_MANIFEST_SHA256 = "manifest_sha256"
23
+ STAMP_KEY_RECORDED_AT_EPOCH = "recorded_at_epoch"
24
+ CODE_REVIEW_BYPASS_MARKER = "# code-review-skip"
25
+ GATED_PUSH_SUBCOMMANDS = frozenset({"push"})
26
+ ALL_GATED_SHELL_TOOL_NAMES = ("Bash", "PowerShell")
27
+ ALL_WRITE_EDIT_TOOL_NAMES = ("Write", "Edit", "MultiEdit")
28
+ MCP_CREATE_PULL_REQUEST_TOOL_NAME = "mcp__plugin_github_github__create_pull_request"
29
+ HASH_PREVIEW_LENGTH = 16
30
+ PRE_TOOL_USE_HOOK_EVENT_NAME = "PreToolUse"
31
+ DENY_PERMISSION_DECISION = "deny"
32
+ PUSH_GATE_HOOK_MODULE_NAME = "code_review_push_gate.py"
33
+ PR_CREATE_GATE_HOOK_MODULE_NAME = "code_review_pr_create_gate.py"
34
+ STAMP_WRITE_BLOCKER_HOOK_MODULE_NAME = "code_review_stamp_directory_write_blocker.py"
35
+ SANCTIONED_STAMP_MINTER_FLAG = "--record-stamp"
36
+ PUSH_GATE_CORRECTIVE_MESSAGE = (
37
+ "BLOCKED: [CODE_REVIEW_PUSH_GATE] This branch surface has no clean "
38
+ "code-review stamp at effort 'low' or higher. Run "
39
+ "`python invoke_code_review.py --record-stamp --cwd <dir> "
40
+ "--session-model <alias> low` (or a higher effort) so a clean review "
41
+ "mints the stamp before `git push`; any file change after minting "
42
+ "invalidates it. Append `# code-review-skip` as a trailing shell comment "
43
+ "only when the verified-commit gate's verify-skip rule for a same-surface "
44
+ "re-land applies. Exempt automatically: docs/image files, pytest test "
45
+ "files, and Python files whose docstring- and comment-stripped AST is "
46
+ "unchanged."
47
+ )
48
+ PR_CREATE_GATE_CORRECTIVE_MESSAGE = (
49
+ "BLOCKED: [CODE_REVIEW_PR_CREATE_GATE] This branch surface has no clean "
50
+ "code-review stamp at effort 'xhigh' or higher. Run "
51
+ "`python invoke_code_review.py --record-stamp --cwd <dir> "
52
+ "--session-model <alias> xhigh` (or max) so a clean review mints the "
53
+ "stamp before `gh pr create` or the MCP create_pull_request tool. Any "
54
+ "file change after minting invalidates the stamp."
55
+ )
56
+ STAMP_DIRECTORY_GUARD_MESSAGE = (
57
+ "BLOCKED: [CODE_REVIEW_STAMP_DIRECTORY_GUARD] Access to the code-review "
58
+ "stamp directory (~/.claude/code-review-stamps/) is denied. Only "
59
+ "invoke_code_review.py --record-stamp mints stamp files; a shell or "
60
+ "Write/Edit forge here would defeat the push and PR-create gates. "
61
+ "Earn a stamp by running a clean /code-review through the invoker."
62
+ )
63
+ RELATIVE_STAMP_DIRECTORY_PATTERN = r"(?:^|(?<=[\s;&|(='\"]))code-review-stamps[\\/]"
64
+ STAMP_DIRECTORY_CHANGE_TARGET_PATTERN = r"[ \t]+['\"]?code-review-stamps[\\/]?['\"]?(?=[\s;&|]|$)"
65
+ STAMP_FILE_REFERENCE_BOUNDARY_PREFIX_PATTERN = r"(?:^|(?<=[\s;&|(='\"\\/]))"
66
+ STAMP_FILE_ROOT_KEY_JSON_SUFFIX_PATTERN = r"[\\/][0-9a-f]{%d}\.json"
67
+ GH_PR_CREATE_INVOCATION_PATTERN = (
68
+ r"(?:^|[;&|\n`({]|\$\()[ \t]*"
69
+ r"(?:(?:if|then|else|elif|while|until|do|!)[ \t]+)*"
70
+ r"(?:[A-Za-z_][A-Za-z0-9_]*=\S+[ \t]+)*"
71
+ r"gh(?:[ \t]+(?:--[A-Za-z][\w-]*(?:=\S+)?|-[A-Za-z])(?:[ \t]+(?!-)\S+)?)*"
72
+ r"[ \t]+pr[ \t]+create\b"
73
+ )
74
+ ALL_STAMP_PATH_SEGMENT_NAMES = (".claude", "code-review-stamps")
75
+ ALL_STAMP_PATH_SEGMENT_BODIES = ("claude", "code-review-stamps")
76
+ STAMP_STORE_MODULE_NAME = "code_review_stamp_store"
77
+ STAMP_MINT_FUNCTION_NAME = "record_clean_stamp"
78
+ ALL_STAMP_STORE_FORGE_PATTERNS = (
79
+ rf"\bimport\s+{STAMP_STORE_MODULE_NAME}\b",
80
+ rf"\bfrom\s+{STAMP_STORE_MODULE_NAME}\s+import\b",
81
+ rf"\b{STAMP_MINT_FUNCTION_NAME}\s*\(",
82
+ )
83
+
84
+
85
+ def effort_meets_threshold(candidate_effort: str, required_effort: str) -> bool:
86
+ """Decide whether a recorded effort satisfies a required threshold.
87
+
88
+ ::
89
+
90
+ effort_meets_threshold("xhigh", "low") -> True xhigh outranks low
91
+ effort_meets_threshold("low", "xhigh") -> False low sits below xhigh
92
+ effort_meets_threshold("ultra", "low") -> False ultra is unranked
93
+
94
+ A candidate satisfies the threshold when it sits at or above the required
95
+ token in the ascending order. A token outside the known order — ``ultra``
96
+ among them — satisfies nothing, so an unrecognized stored effort clears no
97
+ gate.
98
+
99
+ Args:
100
+ candidate_effort: The effort a recorded stamp was earned at.
101
+ required_effort: The effort the gate demands for the action.
102
+
103
+ Returns:
104
+ True when both tokens are known and the candidate ranks at or above the
105
+ required token; False otherwise.
106
+ """
107
+ if candidate_effort not in ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER:
108
+ return False
109
+ if required_effort not in ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER:
110
+ return False
111
+ candidate_rank = ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER.index(candidate_effort)
112
+ required_rank = ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER.index(required_effort)
113
+ return candidate_rank >= required_rank
@@ -0,0 +1,113 @@
1
+ """Behavioral tests for the code-review enforcement constants and comparator.
2
+
3
+ These exercise ``effort_meets_threshold`` against the ordered effort tokens and
4
+ assert the push and pull-request thresholds carry the push-based names the gate
5
+ family reads, so a rename that reintroduces commit terminology fails loudly.
6
+ """
7
+
8
+ import importlib.util
9
+ import pathlib
10
+ import re
11
+
12
+ _CONFIG_DIR = pathlib.Path(__file__).parent
13
+
14
+ SAMPLE_ROOT_KEY_HEX_LENGTH = 16
15
+
16
+ _constants_spec = importlib.util.spec_from_file_location(
17
+ "code_review_enforcement_constants",
18
+ _CONFIG_DIR / "code_review_enforcement_constants.py",
19
+ )
20
+ assert _constants_spec is not None
21
+ assert _constants_spec.loader is not None
22
+ _constants_module = importlib.util.module_from_spec(_constants_spec)
23
+ _constants_spec.loader.exec_module(_constants_module)
24
+
25
+ effort_meets_threshold = _constants_module.effort_meets_threshold
26
+ CODE_REVIEW_ENFORCEMENT_ENABLED = _constants_module.CODE_REVIEW_ENFORCEMENT_ENABLED
27
+ PUSH_REQUIRED_EFFORT = _constants_module.PUSH_REQUIRED_EFFORT
28
+ PR_CREATE_REQUIRED_EFFORT = _constants_module.PR_CREATE_REQUIRED_EFFORT
29
+ GATED_PUSH_SUBCOMMANDS = _constants_module.GATED_PUSH_SUBCOMMANDS
30
+ ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER = _constants_module.ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER
31
+ PUSH_GATE_CORRECTIVE_MESSAGE = _constants_module.PUSH_GATE_CORRECTIVE_MESSAGE
32
+ SANCTIONED_STAMP_MINTER_FLAG = _constants_module.SANCTIONED_STAMP_MINTER_FLAG
33
+ STAMP_DIRECTORY_GUARD_MESSAGE = _constants_module.STAMP_DIRECTORY_GUARD_MESSAGE
34
+ STAMP_DIRECTORY_NAME = _constants_module.STAMP_DIRECTORY_NAME
35
+ GH_PR_CREATE_INVOCATION_PATTERN = _constants_module.GH_PR_CREATE_INVOCATION_PATTERN
36
+ STAMP_FILE_REFERENCE_BOUNDARY_PREFIX_PATTERN = (
37
+ _constants_module.STAMP_FILE_REFERENCE_BOUNDARY_PREFIX_PATTERN
38
+ )
39
+ STAMP_FILE_ROOT_KEY_JSON_SUFFIX_PATTERN = _constants_module.STAMP_FILE_ROOT_KEY_JSON_SUFFIX_PATTERN
40
+
41
+
42
+ def test_gh_pr_create_pattern_matches_create_but_not_edit() -> None:
43
+ invocation_pattern = re.compile(GH_PR_CREATE_INVOCATION_PATTERN, re.IGNORECASE)
44
+ assert invocation_pattern.search("gh pr create --title T --body-file b.md") is not None
45
+ assert invocation_pattern.search("gh pr edit 1 --title T") is None
46
+
47
+
48
+ def test_stamp_file_pattern_fragments_match_a_stamp_file_reference() -> None:
49
+ stamp_file_pattern = re.compile(
50
+ STAMP_FILE_REFERENCE_BOUNDARY_PREFIX_PATTERN
51
+ + re.escape(STAMP_DIRECTORY_NAME)
52
+ + STAMP_FILE_ROOT_KEY_JSON_SUFFIX_PATTERN % SAMPLE_ROOT_KEY_HEX_LENGTH,
53
+ re.IGNORECASE,
54
+ )
55
+ stamp_file_reference = f"cat {STAMP_DIRECTORY_NAME}/{'a' * SAMPLE_ROOT_KEY_HEX_LENGTH}.json"
56
+ assert stamp_file_pattern.search(stamp_file_reference) is not None
57
+ assert stamp_file_pattern.search("cat notes.json") is None
58
+
59
+
60
+ def test_higher_effort_meets_a_lower_threshold() -> None:
61
+ assert effort_meets_threshold("xhigh", "low") is True
62
+
63
+
64
+ def test_lower_effort_fails_a_higher_threshold() -> None:
65
+ assert effort_meets_threshold("low", "xhigh") is False
66
+
67
+
68
+ def test_equal_effort_meets_the_threshold() -> None:
69
+ assert effort_meets_threshold("low", "low") is True
70
+
71
+
72
+ def test_ultra_is_unranked_and_meets_nothing() -> None:
73
+ assert effort_meets_threshold("ultra", "low") is False
74
+
75
+
76
+ def test_unknown_required_effort_meets_nothing() -> None:
77
+ assert effort_meets_threshold("low", "ultra") is False
78
+
79
+
80
+ def test_push_requires_the_lowest_effort_token() -> None:
81
+ every_token_meets_push = all(
82
+ effort_meets_threshold(each_token, PUSH_REQUIRED_EFFORT)
83
+ for each_token in ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER
84
+ )
85
+ assert every_token_meets_push
86
+
87
+
88
+ def test_pr_create_threshold_admits_xhigh_but_rejects_high() -> None:
89
+ assert effort_meets_threshold("xhigh", PR_CREATE_REQUIRED_EFFORT) is True
90
+ assert effort_meets_threshold("high", PR_CREATE_REQUIRED_EFFORT) is False
91
+
92
+
93
+ def test_gated_push_subcommands_gates_push_not_commit() -> None:
94
+ assert "push" in GATED_PUSH_SUBCOMMANDS
95
+ assert "commit" not in GATED_PUSH_SUBCOMMANDS
96
+
97
+
98
+ def test_ultra_absent_from_the_ordered_tokens() -> None:
99
+ assert "ultra" not in ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER
100
+
101
+
102
+ def test_push_gate_message_names_the_push_action_and_low_effort() -> None:
103
+ assert "git push" in PUSH_GATE_CORRECTIVE_MESSAGE
104
+ assert "CODE_REVIEW_PUSH_GATE" in PUSH_GATE_CORRECTIVE_MESSAGE
105
+ assert "'low'" in PUSH_GATE_CORRECTIVE_MESSAGE
106
+
107
+
108
+ def test_guard_message_directs_users_to_the_sanctioned_minter_flag() -> None:
109
+ assert SANCTIONED_STAMP_MINTER_FLAG in STAMP_DIRECTORY_GUARD_MESSAGE
110
+
111
+
112
+ def test_enforcement_defaults_to_off() -> None:
113
+ assert CODE_REVIEW_ENFORCEMENT_ENABLED is False