claude-dev-env 2.2.1 → 2.4.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 (170) hide show
  1. package/CLAUDE.md +3 -2
  2. package/_shared/advisor/advisor-protocol.md +2 -2
  3. package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  4. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -284
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
  6. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +3 -3
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -0
  8. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +5 -0
  9. package/_shared/pr-loop/scripts/stale_worktree_rule_sweep.py +107 -0
  10. package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
  11. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +55 -73
  12. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +9 -15
  13. package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +91 -1
  14. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +88 -1
  15. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -0
  16. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -0
  17. package/_shared/pr-loop/worker-spawn.md +1 -1
  18. package/agents/CLAUDE.md +1 -0
  19. package/agents/code-verifier.md +4 -4
  20. package/agents/skill-writer-agent.md +84 -0
  21. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +141 -41
  22. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
  23. package/docs/CLAUDE.md +1 -0
  24. package/docs/references/CLAUDE.md +1 -0
  25. package/docs/references/code-review-enforcement.md +97 -0
  26. package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
  27. package/hooks/blocking/CLAUDE.md +8 -1
  28. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
  29. package/hooks/blocking/code_review_gate_deny.py +74 -0
  30. package/hooks/blocking/code_review_pr_create_gate.py +194 -0
  31. package/hooks/blocking/code_review_push_gate.py +140 -0
  32. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +340 -0
  33. package/hooks/blocking/code_review_stamp_store.py +233 -0
  34. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
  35. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
  36. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
  37. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
  38. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
  39. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
  40. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
  41. package/hooks/blocking/config/code_review_enforcement_constants.py +110 -0
  42. package/hooks/blocking/config/test_code_review_enforcement_constants.py +108 -0
  43. package/hooks/blocking/config/verified_commit_constants.py +24 -9
  44. package/hooks/blocking/conftest.py +2 -0
  45. package/hooks/blocking/convergence_gate_blocker.py +112 -23
  46. package/hooks/blocking/destructive_command_blocker.py +19 -6
  47. package/hooks/blocking/pr_description_proof_of_work.py +52 -34
  48. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
  49. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
  50. package/hooks/blocking/test_code_review_gate_deny.py +54 -0
  51. package/hooks/blocking/test_code_review_pr_create_gate.py +185 -0
  52. package/hooks/blocking/test_code_review_push_gate.py +189 -0
  53. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +180 -0
  54. package/hooks/blocking/test_code_review_stamp_store.py +205 -0
  55. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
  56. package/hooks/blocking/test_code_verifier_tools_contract.py +28 -0
  57. package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
  58. package/hooks/blocking/test_destructive_command_blocker.py +1 -1
  59. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
  60. package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
  61. package/hooks/blocking/test_pre_tool_use_dispatcher.py +8 -8
  62. package/hooks/blocking/test_verification_verdict_store.py +920 -810
  63. package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
  64. package/hooks/blocking/verification_verdict_store.py +118 -5
  65. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
  66. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
  67. package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
  68. package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
  69. package/hooks/git-hooks/pre_push.py +89 -2
  70. package/hooks/git-hooks/test_pre_push.py +103 -0
  71. package/hooks/hooks.json +16 -1
  72. package/hooks/hooks_constants/CLAUDE.md +1 -0
  73. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
  74. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  75. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
  76. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
  77. package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
  78. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
  79. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  80. package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
  81. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
  82. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
  83. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
  84. package/hooks/validators/CLAUDE.md +1 -0
  85. package/hooks/validators/mypy_integration.py +63 -50
  86. package/hooks/validators/pyproject_config_discovery.py +101 -0
  87. package/hooks/validators/ruff_integration.py +211 -23
  88. package/hooks/validators/run_all_validators.py +21 -14
  89. package/hooks/validators/test_mypy_integration.py +32 -0
  90. package/hooks/validators/test_pyproject_config_discovery.py +94 -0
  91. package/hooks/validators/test_ruff_integration.py +68 -1
  92. package/hooks/validators/test_run_all_validators.py +25 -0
  93. package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
  94. package/package.json +1 -1
  95. package/rules/docstring-prose-matches-implementation.md +20 -43
  96. package/rules/durable-post-artifacts.md +7 -0
  97. package/scripts/CLAUDE.md +2 -1
  98. package/scripts/claude-chain.example.json +15 -3
  99. package/scripts/claude_chain_runner.py +130 -27
  100. package/scripts/claude_chain_usage.py +346 -0
  101. package/scripts/codec_forwarding_test_support.py +83 -0
  102. package/scripts/conftest.py +8 -0
  103. package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -3
  104. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +57 -2
  105. package/scripts/dev_env_scripts_constants/claude_chain_usage_constants.py +58 -0
  106. package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
  107. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
  108. package/scripts/dev_env_scripts_constants/timing.py +1 -1
  109. package/scripts/invoke_code_review.py +550 -38
  110. package/scripts/resolve_worker_spawn.py +8 -1
  111. package/scripts/test_claude_chain_runner.py +412 -6
  112. package/scripts/test_claude_chain_usage.py +534 -0
  113. package/scripts/test_invoke_code_review.py +298 -4
  114. package/scripts/test_invoke_code_review_codec.py +77 -0
  115. package/scripts/test_resolve_worker_spawn_codec.py +101 -0
  116. package/skills/autoconverge/SKILL.md +9 -3
  117. package/skills/autoconverge/reference/convergence.md +2 -1
  118. package/skills/autoconverge/reference/multi-pr.md +6 -1
  119. package/skills/autoconverge/reference/stop-conditions.md +16 -10
  120. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +1 -1
  121. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
  122. package/skills/autoconverge/workflow/converge.contract.test.mjs +19 -0
  123. package/skills/autoconverge/workflow/converge.mjs +24 -8
  124. package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
  125. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
  126. package/skills/fresh-branch/CLAUDE.md +2 -0
  127. package/skills/fresh-branch/SKILL.md +2 -0
  128. package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
  129. package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
  130. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
  131. package/skills/fresh-branch/scripts/pytest.ini +4 -0
  132. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
  133. package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
  134. package/skills/orchestrator/SKILL.md +1 -20
  135. package/skills/orchestrator-refresh/SKILL.md +1 -1
  136. package/skills/pr-converge/SKILL.md +3 -3
  137. package/skills/pr-converge/reference/examples.md +3 -3
  138. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  139. package/skills/pr-converge/reference/ground-rules.md +3 -3
  140. package/skills/pr-converge/reference/per-tick.md +5 -5
  141. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  142. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  143. package/skills/prototype/SKILL.md +86 -0
  144. package/skills/prototype/reference/honest-limitations.md +23 -0
  145. package/skills/prototype/reference/promotion-tasks.md +23 -0
  146. package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
  147. package/skills/prototype/scripts/conftest.py +15 -0
  148. package/skills/prototype/scripts/launch_sandbox.py +205 -0
  149. package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
  150. package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
  151. package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
  152. package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
  153. package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
  154. package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
  155. package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
  156. package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
  157. package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
  158. package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
  159. package/skills/prototype/workflows/promotion.md +27 -0
  160. package/skills/prototype/workflows/sandbox.md +35 -0
  161. package/skills/skill-builder/CLAUDE.md +3 -3
  162. package/skills/skill-builder/SKILL.md +5 -5
  163. package/skills/skill-builder/references/CLAUDE.md +1 -1
  164. package/skills/skill-builder/references/delegation-map.md +3 -3
  165. package/skills/skill-builder/references/description-field.md +1 -1
  166. package/skills/skill-builder/references/skill-modularity.md +2 -3
  167. package/skills/skill-builder/workflows/CLAUDE.md +1 -1
  168. package/skills/skill-builder/workflows/improve-skill.md +1 -1
  169. package/skills/skill-builder/workflows/new-skill.md +2 -2
  170. package/skills/team-advisor/SKILL.md +5 -4
@@ -30,6 +30,8 @@ Exit codes:
30
30
 
31
31
  from __future__ import annotations
32
32
 
33
+ import importlib.util
34
+ import os
33
35
  import subprocess
34
36
  import sys
35
37
  from pathlib import Path
@@ -38,6 +40,12 @@ from git_hooks_constants import (
38
40
  ALL_PROTECTED_BRANCH_PUSH_NAMES,
39
41
  ALL_ZEROS_OBJECT_NAME_CHARACTER,
40
42
  BASE_REFERENCE_ARGUMENT,
43
+ BLOCKING_DIRECTORY_NAME,
44
+ CODE_REVIEW_DENY_REASON_FUNCTION_NAME,
45
+ CODE_REVIEW_PUSH_GATE_MODULE_NAME,
46
+ CODE_REVIEW_PUSH_GATE_PATH_OVERRIDE_ENV_VAR,
47
+ CODE_REVIEW_PUSH_GATE_SCRIPT_FILENAME,
48
+ CODE_REVIEW_STAMP_BLOCK_EXIT_CODE,
41
49
  DEFAULT_REMOTE_BASE_REFERENCE,
42
50
  GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE,
43
51
  INVOKE_GATE_FAILURE_MESSAGE,
@@ -158,6 +166,82 @@ def invoke_gate(gate_script_path: Path, base_reference: str) -> int:
158
166
  return completion.returncode
159
167
 
160
168
 
169
+ def resolve_code_review_gate_path() -> tuple[Path, Path | None]:
170
+ """Return the code-review push gate script path and its exact-allow override.
171
+
172
+ The override environment variable, when set, names the only path the trust
173
+ check accepts; otherwise the gate script resolves to the ``blocking``
174
+ directory beside this hook's ``git-hooks`` directory, which holds in both
175
+ the repository layout and the installed ``~/.claude/hooks`` layout.
176
+
177
+ Returns:
178
+ A pair of the resolved gate script path and either the exact override
179
+ path (override set) or None (trust-root case).
180
+ """
181
+ override_path_raw = os.environ.get(
182
+ CODE_REVIEW_PUSH_GATE_PATH_OVERRIDE_ENV_VAR, ""
183
+ ).strip()
184
+ if override_path_raw:
185
+ exact_override = Path(override_path_raw).resolve()
186
+ return exact_override, exact_override
187
+ blocking_directory = Path(__file__).resolve().parent.parent / BLOCKING_DIRECTORY_NAME
188
+ return blocking_directory / CODE_REVIEW_PUSH_GATE_SCRIPT_FILENAME, None
189
+
190
+
191
+ def load_code_review_deny_reason(gate_script_path: Path, work_tree_directory: str) -> str | None:
192
+ """Import the gate script and read its push deny reason for a work tree.
193
+
194
+ Any import or evaluation failure reads as no deny reason.
195
+
196
+ Args:
197
+ gate_script_path: The resolved code-review push gate script path.
198
+ work_tree_directory: The work tree the push targets.
199
+
200
+ Returns:
201
+ The gate's deny reason when the surface lacks a covering low stamp;
202
+ None when the gate allows the push or could not be evaluated.
203
+ """
204
+ try:
205
+ gate_specification = importlib.util.spec_from_file_location(
206
+ CODE_REVIEW_PUSH_GATE_MODULE_NAME, str(gate_script_path)
207
+ )
208
+ if gate_specification is None or gate_specification.loader is None:
209
+ return None
210
+ gate_module = importlib.util.module_from_spec(gate_specification)
211
+ gate_specification.loader.exec_module(gate_module)
212
+ deny_reason_function = getattr(gate_module, CODE_REVIEW_DENY_REASON_FUNCTION_NAME, None)
213
+ if deny_reason_function is None:
214
+ return None
215
+ deny_reason = deny_reason_function(work_tree_directory)
216
+ except Exception:
217
+ return None
218
+ if isinstance(deny_reason, str) and deny_reason:
219
+ return deny_reason
220
+ return None
221
+
222
+
223
+ def code_review_stamp_block_exit_code() -> int:
224
+ """Block the push when no clean low code-review stamp covers the surface.
225
+
226
+ Runs the gate decision only when the gate script sits at a trusted
227
+ installed location (or the exact override path). When the gate is absent
228
+ from a trusted location, the check is skipped and the push is allowed,
229
+ matching the CODE_RULES gate's fail-open posture.
230
+
231
+ Returns:
232
+ The block exit code when a covering low stamp is missing; 0 when the
233
+ surface is covered or the gate is not installed at a trusted location.
234
+ """
235
+ gate_script_path, exact_allowed_path = resolve_code_review_gate_path()
236
+ if not is_safe_regular_file(gate_script_path, exact_allowed_path):
237
+ return 0
238
+ deny_reason = load_code_review_deny_reason(gate_script_path, os.getcwd())
239
+ if deny_reason is None:
240
+ return 0
241
+ sys.stderr.write(deny_reason + "\n")
242
+ return CODE_REVIEW_STAMP_BLOCK_EXIT_CODE
243
+
244
+
161
245
  def main() -> int:
162
246
  stdin_read_failure_message = STDIN_READ_FAILURE_MESSAGE
163
247
  gate_infrastructure_failure_exit_code = GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE
@@ -191,14 +275,17 @@ def main() -> int:
191
275
  pre_push_gate_script_not_found_message.format(path=gate_script_path),
192
276
  file=sys.stderr,
193
277
  )
194
- return 0
278
+ return code_review_stamp_block_exit_code()
195
279
  base_reference = resolve_base_reference_from_stdin(stdin_text)
196
280
  if base_reference is None:
197
281
  return 0
198
282
  if base_reference == no_parseable_stdin_lines_sentinel:
199
283
  print(no_parseable_stdin_lines_message, file=sys.stderr)
200
284
  return gate_infrastructure_failure_exit_code
201
- return invoke_gate(gate_script_path, base_reference)
285
+ code_rules_exit_code = invoke_gate(gate_script_path, base_reference)
286
+ if code_rules_exit_code != 0:
287
+ return code_rules_exit_code
288
+ return code_review_stamp_block_exit_code()
202
289
 
203
290
 
204
291
  if __name__ == "__main__":
@@ -28,6 +28,12 @@ NON_ZERO_REMOTE_SHA_ONE: str = "1" * 40
28
28
  NON_ZERO_REMOTE_SHA_TWO: str = "2" * 40
29
29
 
30
30
 
31
+ def _isolate_code_review_gate(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
32
+ monkeypatch.setenv(
33
+ "CODE_REVIEW_PUSH_GATE_PATH", str(tmp_path / "no_code_review_gate.py")
34
+ )
35
+
36
+
31
37
  def test_resolve_base_reference_uses_remote_object_when_non_zero() -> None:
32
38
  stdin_text = (
33
39
  f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {NON_ZERO_REMOTE_SHA_ONE}\n"
@@ -73,6 +79,7 @@ def test_main_exits_zero_when_gate_script_missing(
73
79
  "CODE_RULES_GATE_PATH",
74
80
  str(tmp_path / "does_not_exist.py"),
75
81
  )
82
+ _isolate_code_review_gate(tmp_path, monkeypatch)
76
83
  monkeypatch.setattr(sys, "stdin", io.StringIO(""))
77
84
 
78
85
  exit_code = pre_push.main()
@@ -94,6 +101,7 @@ def test_main_invokes_gate_with_resolved_base_reference(
94
101
  encoding="utf-8",
95
102
  )
96
103
  monkeypatch.setenv("CODE_RULES_GATE_PATH", str(recording_gate_script_path))
104
+ _isolate_code_review_gate(tmp_path, monkeypatch)
97
105
  remote_sha = "9" * 40
98
106
  monkeypatch.setattr(
99
107
  sys,
@@ -312,6 +320,7 @@ def test_invoke_gate_uses_resolved_path(
312
320
  symlink_gate_path.symlink_to(real_gate_path)
313
321
  resolved_path = symlink_gate_path.resolve()
314
322
  monkeypatch.setenv("CODE_RULES_GATE_PATH", str(symlink_gate_path))
323
+ _isolate_code_review_gate(tmp_path, monkeypatch)
315
324
  monkeypatch.setattr(sys, "stdin", io.StringIO(
316
325
  f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {NON_ZERO_REMOTE_SHA_ONE}\n"
317
326
  ))
@@ -428,6 +437,7 @@ def test_main_allows_main_onto_main_push(
428
437
  passing_gate_path = tmp_path / "gate.py"
429
438
  passing_gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
430
439
  monkeypatch.setenv("CODE_RULES_GATE_PATH", str(passing_gate_path))
440
+ _isolate_code_review_gate(tmp_path, monkeypatch)
431
441
  monkeypatch.setattr(
432
442
  sys,
433
443
  "stdin",
@@ -439,3 +449,96 @@ def test_main_allows_main_onto_main_push(
439
449
  exit_code = pre_push.main()
440
450
 
441
451
  assert exit_code == 0
452
+
453
+
454
+ CODE_REVIEW_STUB_BLOCK_REASON: str = "CODE_REVIEW_STUB_BLOCK_REASON"
455
+
456
+
457
+ def _write_passing_code_rules_gate(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
458
+ passing_gate_path = tmp_path / "code_rules_gate.py"
459
+ passing_gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
460
+ monkeypatch.setenv("CODE_RULES_GATE_PATH", str(passing_gate_path))
461
+ monkeypatch.setattr(
462
+ sys,
463
+ "stdin",
464
+ io.StringIO(
465
+ f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {NON_ZERO_REMOTE_SHA_ONE}\n"
466
+ ),
467
+ )
468
+
469
+
470
+ def _write_code_review_gate_stub(tmp_path: Path, returned_reason_literal: str) -> Path:
471
+ stub_gate_path = tmp_path / "code_review_push_gate.py"
472
+ stub_gate_path.write_text(
473
+ "def deny_reason_for_directory(target_directory):\n"
474
+ f" return {returned_reason_literal}\n",
475
+ encoding="utf-8",
476
+ )
477
+ return stub_gate_path
478
+
479
+
480
+ def test_main_blocks_when_code_review_gate_returns_reason(
481
+ tmp_path: Path,
482
+ monkeypatch: pytest.MonkeyPatch,
483
+ capsys: pytest.CaptureFixture[str],
484
+ ) -> None:
485
+ _write_passing_code_rules_gate(tmp_path, monkeypatch)
486
+ stub_gate_path = _write_code_review_gate_stub(
487
+ tmp_path, repr(CODE_REVIEW_STUB_BLOCK_REASON)
488
+ )
489
+ monkeypatch.setenv("CODE_REVIEW_PUSH_GATE_PATH", str(stub_gate_path))
490
+
491
+ exit_code = pre_push.main()
492
+
493
+ assert exit_code == git_hooks_constants.CODE_REVIEW_STAMP_BLOCK_EXIT_CODE
494
+ captured = capsys.readouterr()
495
+ assert CODE_REVIEW_STUB_BLOCK_REASON in captured.err
496
+
497
+
498
+ def test_main_allows_when_code_review_gate_returns_none(
499
+ tmp_path: Path,
500
+ monkeypatch: pytest.MonkeyPatch,
501
+ ) -> None:
502
+ _write_passing_code_rules_gate(tmp_path, monkeypatch)
503
+ stub_gate_path = _write_code_review_gate_stub(tmp_path, "None")
504
+ monkeypatch.setenv("CODE_REVIEW_PUSH_GATE_PATH", str(stub_gate_path))
505
+
506
+ exit_code = pre_push.main()
507
+
508
+ assert exit_code == 0
509
+
510
+
511
+ def test_main_code_review_check_fails_open_when_gate_module_absent(
512
+ tmp_path: Path,
513
+ monkeypatch: pytest.MonkeyPatch,
514
+ ) -> None:
515
+ _write_passing_code_rules_gate(tmp_path, monkeypatch)
516
+ monkeypatch.setenv(
517
+ "CODE_REVIEW_PUSH_GATE_PATH", str(tmp_path / "does_not_exist.py")
518
+ )
519
+
520
+ exit_code = pre_push.main()
521
+
522
+ assert exit_code == 0
523
+
524
+
525
+ def test_main_allows_deletion_push_even_when_code_review_gate_would_block(
526
+ tmp_path: Path,
527
+ monkeypatch: pytest.MonkeyPatch,
528
+ ) -> None:
529
+ passing_gate_path = tmp_path / "code_rules_gate.py"
530
+ passing_gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
531
+ monkeypatch.setenv("CODE_RULES_GATE_PATH", str(passing_gate_path))
532
+ stub_gate_path = _write_code_review_gate_stub(
533
+ tmp_path, repr(CODE_REVIEW_STUB_BLOCK_REASON)
534
+ )
535
+ monkeypatch.setenv("CODE_REVIEW_PUSH_GATE_PATH", str(stub_gate_path))
536
+ deletion_stdin = (
537
+ f"refs/heads/feature {ALL_ZEROS_OBJECT_NAME} "
538
+ f"refs/heads/feature {ALL_ZEROS_OBJECT_NAME}\n"
539
+ )
540
+ monkeypatch.setattr(sys, "stdin", io.StringIO(deletion_stdin))
541
+
542
+ exit_code = pre_push.main()
543
+
544
+ assert exit_code == 0
package/hooks/hooks.json CHANGED
@@ -57,6 +57,16 @@
57
57
  }
58
58
  ]
59
59
  },
60
+ {
61
+ "matcher": "mcp__plugin_github_github__create_pull_request",
62
+ "hooks": [
63
+ {
64
+ "type": "command",
65
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/code_review_pr_create_gate.py",
66
+ "timeout": 20
67
+ }
68
+ ]
69
+ },
60
70
  {
61
71
  "matcher": "mcp__plugin_github_github__.*",
62
72
  "hooks": [
@@ -79,7 +89,12 @@
79
89
  "type": "command",
80
90
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/pr_converge_bugteam_enforcer.py",
81
91
  "timeout": 10
82
- },
92
+ }
93
+ ]
94
+ },
95
+ {
96
+ "matcher": "Agent|Task",
97
+ "hooks": [
83
98
  {
84
99
  "type": "command",
85
100
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/code_verifier_spawn_preflight_gate.py",
@@ -60,6 +60,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
60
60
  | `pre_tool_use_stdin.py` | `read_hook_input_dictionary_from_stdin()` — shared stdin parser for PreToolUse hooks |
61
61
  | `precommit_code_rules_gate_constants.py` | Scope argument and exit-code constants for the precommit gate |
62
62
  | `project_paths_reader.py` | Loads `~/.claude/project-paths.json` — the per-user project-path registry |
63
+ | `pyproject_config_discovery_constants.py` | Table names (``tool`` key, ``mypy``, ``ruff``) for resolving a validator's config from a pyproject.toml `[tool.<name>]` table |
63
64
  | `pytest_testpaths_orphan_blocker_constants.py` | Marker filename, section and key names, test-file pattern, search budget, and block-message text for the pytest unregistered-test-directory blocker |
64
65
  | `python_style_checks_constants.py` | Command-line argument count and blank-line count between top-level functions for the style validator |
65
66
  | `reviewer_spawn_gate_constants.py` | Bash tool name, the sentinel marker, the Copilot and Bugbot trigger markers, the availability-script relative path and override env-var name, and the deny-message template for the reviewer-spawn gate |
@@ -75,4 +75,12 @@ ALL_BASH_HOSTED_HOOK_ENTRIES: tuple[BashHostedHookEntry, ...] = (
75
75
  BashHostedHookEntry(
76
76
  "blocking/verdict_directory_write_blocker.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES
77
77
  ),
78
+ BashHostedHookEntry("blocking/code_review_push_gate.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES),
79
+ BashHostedHookEntry(
80
+ "blocking/code_review_pr_create_gate.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES
81
+ ),
82
+ BashHostedHookEntry(
83
+ "blocking/code_review_stamp_directory_write_blocker.py",
84
+ ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
85
+ ),
78
86
  )
@@ -22,6 +22,7 @@ ALL_CONFIG_DIRECTORY_NAMES = frozenset(
22
22
  "pr_converge_scripts_constants",
23
23
  "bugteam_scripts_constants",
24
24
  "usage_pause_constants",
25
+ "prototype_scripts_constants",
25
26
  "codex_review_scripts_constants",
26
27
  "dev_env_scripts_constants",
27
28
  "fresh_branch_scripts_constants",
@@ -1,21 +1,29 @@
1
1
  """Configuration constants for the code_verifier_spawn_preflight_gate hook.
2
2
 
3
- The gate denies an ``Agent`` spawn whose ``subagent_type`` is ``code-verifier``
4
- when the branch carries a merge conflict against its base ref or a CODE_RULES
5
- violation on a line added in the working tree since the merge base (committed on
6
- the branch or uncommitted). It runs two
7
- pre-flight checks before the expensive verification spawn and addresses its
8
- deny reason to the spawning agent so that agent fixes the named issues and
9
- re-spawns. Every literal the hook body reads lives here; the hook imports
10
- ``AGENT_TOOL_NAME`` from ``pr_converge_bugteam_enforcer_constants`` rather than
11
- redefining it.
3
+ The gate denies an ``Agent`` or ``Task`` spawn whose ``subagent_type`` is
4
+ ``code-verifier`` when the branch carries a merge conflict against its base
5
+ ref, a CODE_RULES violation on a line added in the working tree since the merge
6
+ base (committed on the branch or uncommitted), or a CODE_RULES engine
7
+ import/load failure. It runs those pre-flight checks before the expensive
8
+ verification spawn and addresses its deny reason to the spawning agent so that
9
+ agent fixes the named issues and re-spawns. Environmental git failures stay
10
+ fail-open; engine import/load failure is fail-closed. Every literal the hook
11
+ body reads lives here; ``AGENT_TOOL_NAME`` comes from
12
+ ``pr_converge_bugteam_enforcer_constants`` and joins ``TASK_TOOL_NAME`` in
13
+ ``ALL_CODE_VERIFIER_SPAWN_TOOL_NAMES`` so both spawn tool names share one source.
12
14
  """
13
15
 
14
16
  from __future__ import annotations
15
17
 
16
18
  from pathlib import Path
17
19
 
20
+ from hooks_constants.pr_converge_bugteam_enforcer_constants import AGENT_TOOL_NAME
21
+
18
22
  CODE_VERIFIER_SUBAGENT_TYPE: str = "code-verifier"
23
+ TASK_TOOL_NAME: str = "Task"
24
+ ALL_CODE_VERIFIER_SPAWN_TOOL_NAMES: frozenset[str] = frozenset(
25
+ {AGENT_TOOL_NAME, TASK_TOOL_NAME}
26
+ )
19
27
 
20
28
  ALL_MERGE_TREE_COMMAND_FLAGS: tuple[str, ...] = (
21
29
  "merge-tree",
@@ -39,11 +47,18 @@ ALL_NAME_ONLY_WORKTREE_DIFF_FLAGS: tuple[str, ...] = (
39
47
  ALL_UNIFIED_ZERO_DIFF_FLAGS: tuple[str, ...] = ("diff", "--unified=0")
40
48
 
41
49
  DENY_REASON_LEAD: str = (
42
- "BLOCKED [code-verifier-spawn-preflight]: an Agent spawn with subagent_type "
43
- "code-verifier is blocked because the branch is not in a committable state. "
50
+ "BLOCKED [code-verifier-spawn-preflight]: a code-verifier spawn "
51
+ "(Agent or Task tool, subagent_type code-verifier) is blocked because "
52
+ "the branch is not in a committable state. "
44
53
  "Fix these, then re-spawn the code-verifier:"
45
54
  )
46
55
  MERGE_CONFLICT_SECTION_HEADER: str = "Merge conflicts vs {base_ref}:"
47
56
  CODE_RULES_SECTION_HEADER: str = "CODE_RULES violations on changed lines:"
57
+ ENGINE_LOAD_FAILURE_SECTION: str = (
58
+ "CODE_RULES engine failed to load:\n"
59
+ " The CODE_RULES validate_content engine could not be imported or loaded "
60
+ "(package import/load failure). Repair the enforcer so load_validate_content "
61
+ "succeeds, then re-spawn code-verifier."
62
+ )
48
63
 
49
64
  GATE_SCRIPTS_RELATIVE_PATH: Path = Path("_shared") / "pr-loop" / "scripts"
@@ -25,12 +25,29 @@ chained command does not bind the gate to the wrong PR::
25
25
 
26
26
  GH_PR_READY_ANCHOR_PATTERN: str = r"\bgh\s+pr\s+ready\b(?![^&|;\n]*--undo)"
27
27
 
28
- COMMAND_SEPARATOR_PATTERN: str = r"&&|\|\||;|\||\n"
28
+ COMMAND_SEPARATOR_PATTERN: str = r"&&|\|\||;|\||\n|(?<![>&])&(?![>&])"
29
+
30
+ BASH_LINE_CONTINUATION_PATTERN: str = r"\\\r?\n[ \t]*"
29
31
 
30
32
  PR_URL_OWNER_REPO_NUMBER_PATTERN: str = (
31
- r"https://github\.com/(?P<owner>[\w.-]+)/(?P<repo>[\w.-]+)/pull/(?P<number>\d+)"
33
+ r"https?://[^/]+/(?P<owner>[\w.-]+)/(?P<repo>[\w.-]+)/pull/(?P<number>\d+)"
32
34
  )
33
35
 
34
36
  REPO_OVERRIDE_FLAG_PATTERN: str = (
35
- r"(?:^|\s)(?:--repo|-R)(?:=|\s+)(?:[\w.-]+/)*(?P<owner>[\w.-]+)/(?P<repo>[\w.-]+)"
37
+ r"(?:^|\s)(?:--repo(?:=|\s+)|-R(?:=|\s+)?)"
38
+ r"(?:https?://[^/\s]+/|git@[^:\s]+:|(?:[\w.-]+/)*)"
39
+ r"(?P<owner>[\w.-]+)/(?P<repo>[\w.-]+)"
40
+ )
41
+
42
+ ALL_GH_PR_VIEW_NUMBER_COMMAND: tuple[str, ...] = (
43
+ "gh",
44
+ "pr",
45
+ "view",
46
+ "--json",
47
+ "number",
48
+ "--jq",
49
+ ".number",
36
50
  )
51
+ GH_REPO_FLAG: str = "--repo"
52
+ REPO_SLUG_TEMPLATE: str = "{owner}/{repo}"
53
+ GIT_URL_SUFFIX: str = ".git"
@@ -1,4 +1,4 @@
1
- """Segment-splitting and command-name constants for the destructive command blocker compound rm guard."""
1
+ """Segment-splitting, command-name, and deny-mode constants for the destructive command blocker."""
2
2
 
3
3
  ALL_SHELL_CONTROL_OPERATOR_TOKENS: frozenset[str] = frozenset({"&&", "||", ";", "|&", "|", "&", "\n", "\r"})
4
4
  ALL_COMMAND_LAUNCHER_WRAPPER_COMMANDS: frozenset[str] = frozenset(
@@ -191,3 +191,5 @@ ALL_KNOWN_TEMPORARY_ENVIRONMENT_VARIABLE_NAMES: frozenset[str] = frozenset(
191
191
  "CLAUDE_JOB_DIR",
192
192
  }
193
193
  )
194
+ DESTRUCTIVE_DENY_MODE_ENV_VAR: str = "CLAUDE_DESTRUCTIVE_DENY_MODE"
195
+ ALL_TRUTHY_ENV_VALUES: frozenset[str] = frozenset({"1", "true", "yes", "on"})
@@ -22,8 +22,6 @@ GH_SLURP_FLAG: str = "--slurp"
22
22
  PR_COMMENTS_API_PATH_TEMPLATE: str = "repos/{{owner}}/{{repo}}/issues/{pr_number}/comments"
23
23
  ALL_PR_DIFF_SUBCOMMANDS: tuple[str, ...] = ("pr", "diff")
24
24
  PR_DIFF_NAME_ONLY_FLAG: str = "--name-only"
25
- ALL_PR_VIEW_NUMBER_ARGUMENTS: tuple[str, ...] = ("pr", "view", "--json", "number")
26
- PR_NUMBER_JSON_FIELD: str = "number"
27
25
  COMMENT_BODY_JSON_FIELD: str = "body"
28
26
  GH_COMMAND_TIMEOUT_SECONDS: int = 6
29
27
  MAX_DIFF_SCAN_CHARS: int = 200000
@@ -83,7 +81,6 @@ __all__ = [
83
81
  "ALL_HONEST_GAP_PHRASES",
84
82
  "ALL_PROOF_HEADING_KEYWORDS",
85
83
  "ALL_PR_DIFF_SUBCOMMANDS",
86
- "ALL_PR_VIEW_NUMBER_ARGUMENTS",
87
84
  "ALL_VISUAL_FILE_SUFFIXES",
88
85
  "COMMENT_BODY_JSON_FIELD",
89
86
  "DIGIT_PATTERN",
@@ -104,7 +101,6 @@ __all__ = [
104
101
  "PROOF_PART_VISUAL_MESSAGE",
105
102
  "PR_COMMENTS_API_PATH_TEMPLATE",
106
103
  "PR_DIFF_NAME_ONLY_FLAG",
107
- "PR_NUMBER_JSON_FIELD",
108
104
  "PR_READY_GATE_MESSAGE_TEMPLATE",
109
105
  "PR_READY_INVOCATION_PATTERN",
110
106
  "PR_READY_UNDO_FLAG",
@@ -80,6 +80,10 @@ ALL_HOSTED_HOOK_ENTRIES: tuple[HostedHookEntry, ...] = (
80
80
  script_relative_path="blocking/sensitive_file_protector.py",
81
81
  applicable_tool_names=ALL_WRITE_AND_EDIT_TOOL_NAMES,
82
82
  ),
83
+ HostedHookEntry(
84
+ script_relative_path="blocking/code_review_stamp_directory_write_blocker.py",
85
+ applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
86
+ ),
83
87
  HostedHookEntry(
84
88
  script_relative_path="blocking/pii_prevention_blocker.py",
85
89
  applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
@@ -0,0 +1,16 @@
1
+ """Table names used when a validator resolves its config from a pyproject.toml.
2
+
3
+ A ``pyproject.toml`` nests each tool's settings under ``[tool.<name>]``, so the
4
+ top-level table key is ``tool`` and each tool matches its own name below it.
5
+ These names live here so the discovery module carries no inline table literals.
6
+ """
7
+
8
+ __all__ = [
9
+ "TOOL_TABLE_KEY",
10
+ "MYPY_TOOL_TABLE_NAME",
11
+ "RUFF_TOOL_TABLE_NAME",
12
+ ]
13
+
14
+ TOOL_TABLE_KEY: str = "tool"
15
+ MYPY_TOOL_TABLE_NAME: str = "mypy"
16
+ RUFF_TOOL_TABLE_NAME: str = "ruff"
@@ -40,12 +40,18 @@ _EXPECTED_BASH_ORDER = (
40
40
  "blocking/gh_pr_author_enforcer.py",
41
41
  "blocking/verified_commit_gate.py",
42
42
  "blocking/verdict_directory_write_blocker.py",
43
+ "blocking/code_review_push_gate.py",
44
+ "blocking/code_review_pr_create_gate.py",
45
+ "blocking/code_review_stamp_directory_write_blocker.py",
43
46
  )
44
47
 
45
48
  _POWERSHELL_APPLICABLE = (
46
49
  "blocking/pii_prevention_blocker.py",
47
50
  "blocking/verified_commit_gate.py",
48
51
  "blocking/verdict_directory_write_blocker.py",
52
+ "blocking/code_review_push_gate.py",
53
+ "blocking/code_review_pr_create_gate.py",
54
+ "blocking/code_review_stamp_directory_write_blocker.py",
49
55
  )
50
56
 
51
57
 
@@ -80,6 +86,24 @@ def test_powershell_hooks_carry_the_shared_tool_set() -> None:
80
86
  assert each_entry.applicable_tool_names == ALL_BASH_AND_POWERSHELL_TOOL_NAMES
81
87
 
82
88
 
89
+ def test_code_review_gate_family_registered_for_bash_and_powershell() -> None:
90
+ """The three code-review shell gates run on both Bash and PowerShell."""
91
+ code_review_gate_paths = (
92
+ "blocking/code_review_push_gate.py",
93
+ "blocking/code_review_pr_create_gate.py",
94
+ "blocking/code_review_stamp_directory_write_blocker.py",
95
+ )
96
+ entry_by_path = {
97
+ each_entry.script_relative_path: each_entry
98
+ for each_entry in ALL_BASH_HOSTED_HOOK_ENTRIES
99
+ }
100
+ for each_gate_path in code_review_gate_paths:
101
+ assert each_gate_path in entry_by_path
102
+ assert entry_by_path[each_gate_path].applicable_tool_names == (
103
+ ALL_BASH_AND_POWERSHELL_TOOL_NAMES
104
+ )
105
+
106
+
83
107
  def test_every_roster_path_points_at_an_existing_hook() -> None:
84
108
  """Each roster path names a hook script present on disk, catching a typo."""
85
109
  hooks_root = Path(__file__).resolve().parent.parent
@@ -63,6 +63,12 @@ def test_duplicate_rmtree_helper_blocker_runs_via_runpy() -> None:
63
63
  assert entry.native_module_name is None
64
64
 
65
65
 
66
+ def test_code_review_stamp_write_blocker_registered_for_all_file_tools() -> None:
67
+ entry = _entry_for("blocking/code_review_stamp_directory_write_blocker.py")
68
+ assert entry is not None
69
+ assert {"Write", "Edit", "MultiEdit"} <= entry.applicable_tool_names
70
+
71
+
66
72
  def test_windows_rmtree_blocker_still_registered() -> None:
67
73
  entry = _entry_for("blocking/windows_rmtree_blocker.py")
68
74
  assert entry is not None
@@ -19,9 +19,16 @@ ALL_GH_POST_SUBCOMMANDS: dict[str, frozenset[str]] = {
19
19
  "issue": frozenset({"create", "comment", "edit"}),
20
20
  }
21
21
 
22
- ALL_VOLATILE_PATH_MARKERS: tuple[str, ...] = (
22
+ PATH_ANCHOR_CHARACTER: str = "/"
23
+
24
+ PATH_SEGMENT_START_CHARACTERS: str = "_-"
25
+
26
+ ALL_PATH_ANCHORED_VOLATILE_PATH_MARKERS: tuple[str, ...] = (
23
27
  ".claude-editor/jobs/",
24
28
  ".claude/worktrees/",
29
+ )
30
+
31
+ ALL_BARE_VOLATILE_PATH_MARKERS: tuple[str, ...] = (
25
32
  "appdata/local/temp",
26
33
  "/tmp/",
27
34
  "%temp%",
@@ -12,6 +12,7 @@ A library of check modules used by the validation hooks. Each module focuses on
12
12
  | `output_formatter.py` | Formats `Violation` lists into human-readable output |
13
13
  | `run_all_validators.py` | Entry point — runs every check module and aggregates results |
14
14
  | `health_check.py` | Verifies that all validator dependencies (ruff, mypy) are reachable |
15
+ | `pyproject_config_discovery.py` | Shared walk-up primitive that resolves a tool's pyproject.toml config from an original target path, matching the `[tool.<name>]` table the tool owns |
15
16
  | `python_style_helpers.py` | Shared source-line splitting and function-discovery helpers imported by `python_style_checks.py` |
16
17
 
17
18
  ## Check modules