claude-dev-env 2.12.0 → 2.13.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 (193) hide show
  1. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  2. package/_shared/pr-loop/scripts/test_code_rules_gate.py +0 -26
  3. package/agents/CLAUDE.md +1 -2
  4. package/agents/pr-description-writer.md +11 -81
  5. package/bin/install.mjs +48 -9
  6. package/bin/install.prune.test.mjs +26 -0
  7. package/bin/install.settings-defaults.test.mjs +60 -33
  8. package/bin/install.test.mjs +2 -4
  9. package/commands/sr-loop.md +9 -5
  10. package/docs/CLAUDE.md +0 -1
  11. package/docs/references/CLAUDE.md +0 -1
  12. package/hooks/CLAUDE.md +1 -1
  13. package/hooks/blocking/CLAUDE.md +2 -27
  14. package/hooks/blocking/config/CLAUDE.md +1 -13
  15. package/hooks/blocking/conventional_pr_title_gate.py +1 -2
  16. package/hooks/blocking/send_user_file_open_locally_blocker.py +1 -1
  17. package/hooks/blocking/stop_dispatcher.py +4 -4
  18. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -3
  19. package/hooks/blocking/test_pre_tool_use_dispatcher.py +6 -7
  20. package/hooks/blocking/test_send_user_file_open_locally_blocker.py +4 -3
  21. package/hooks/blocking/test_shared_stdin_adoption.py +0 -2
  22. package/hooks/diagnostic/CLAUDE.md +3 -3
  23. package/hooks/diagnostic/hook_log_extractor.py +2 -36
  24. package/hooks/diagnostic/hook_log_stop_wrapper.py +6 -155
  25. package/hooks/diagnostic/test_hook_log_extractor.py +8 -21
  26. package/hooks/diagnostic/test_hook_log_stop_wrapper.py +3 -331
  27. package/hooks/git-hooks/git_hooks_constants/__init__.py +0 -6
  28. package/hooks/git-hooks/pre_push.py +3 -85
  29. package/hooks/git-hooks/test_pre_push.py +0 -130
  30. package/hooks/hooks.json +0 -35
  31. package/hooks/hooks_constants/CLAUDE.md +0 -9
  32. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -11
  33. package/hooks/hooks_constants/convergence_branch_constants.py +0 -1
  34. package/hooks/hooks_constants/fable_spawn_gate_constants.py +3 -4
  35. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -8
  36. package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +1 -6
  37. package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -1
  38. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -26
  39. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +0 -5
  40. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -1
  41. package/hooks/lifecycle/CLAUDE.md +1 -3
  42. package/package.json +1 -1
  43. package/rules/CLAUDE.md +0 -2
  44. package/rules/gh-cli-conventions.md +0 -1
  45. package/rules/git-workflow.md +1 -3
  46. package/scripts/CLAUDE.md +0 -1
  47. package/scripts/_code_review_test_support.py +0 -95
  48. package/scripts/dev_env_scripts_constants/code_review_constants.py +9 -98
  49. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +1 -44
  50. package/scripts/invoke_code_review.py +8 -451
  51. package/scripts/test_invoke_code_review.py +5 -61
  52. package/scripts/test_invoke_code_review_cli.py +1 -45
  53. package/scripts/test_invoke_code_review_contract.py +1 -118
  54. package/settings.json +1 -10
  55. package/skills/CLAUDE.md +8 -0
  56. package/skills/autoconverge/reference/CLAUDE.md +1 -1
  57. package/skills/autoconverge/reference/convergence.md +7 -7
  58. package/skills/autoconverge/reference/gotchas.md +3 -10
  59. package/skills/autoconverge/workflow/converge.contract.test.mjs +53 -1388
  60. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +4 -3
  61. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +33 -493
  62. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +15 -97
  63. package/skills/autoconverge/workflow/converge.mjs +201 -238
  64. package/skills/autoconverge/workflow/converge.precatch.test.mjs +1 -1
  65. package/skills/bugteam/CONSTRAINTS.md +1 -1
  66. package/skills/bugteam/SKILL.md +9 -0
  67. package/skills/bugteam/reference/audit-and-teammates.md +10 -6
  68. package/skills/bugteam/test_skill_additions.py +9 -0
  69. package/skills/comments/SKILL.md +65 -0
  70. package/skills/descriptions/SKILL.md +51 -0
  71. package/skills/emergencies/SKILL.md +42 -0
  72. package/skills/imagegen/SKILL.md +21 -0
  73. package/skills/imagegen/scripts/config/__init__.py +1 -0
  74. package/skills/imagegen/scripts/config/constants.py +28 -0
  75. package/skills/imagegen/scripts/imagegen.py +45 -0
  76. package/skills/imagegen/scripts/imagegen_core.py +528 -0
  77. package/skills/imagegen/scripts/test_imagegen.py +204 -0
  78. package/skills/orchestrator/SKILL.md +2 -4
  79. package/skills/plan-to-pr/SKILL.md +6 -7
  80. package/skills/plan-to-pr/reference/final-validation-tasks.md +1 -1
  81. package/skills/plan-to-pr/reference/process-inventory.md +2 -2
  82. package/skills/plan-to-pr/reference/review-loop.md +2 -2
  83. package/skills/plan-to-pr/reference/run-record.schema.json +1 -1
  84. package/skills/plan-to-pr/reference/task-seeds.md +1 -1
  85. package/skills/plan-to-pr/reference/task-ticket.md +3 -4
  86. package/skills/plan-to-pr/scripts/config/constants.py +1 -2
  87. package/skills/plan-to-pr/scripts/test_validate_protocol.py +1 -1
  88. package/skills/plan-to-pr/test_skill_contract.py +2 -2
  89. package/skills/pr-converge/SKILL.md +13 -16
  90. package/skills/pr-converge/reference/progress-checklist.md +6 -5
  91. package/skills/pr-converge/reference/state-schema.md +1 -27
  92. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  93. package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +3 -3
  94. package/skills/pr-loop-lifecycle/SKILL.md +1 -1
  95. package/skills/pr-loop-lifecycle/reference/teardown-publish-permissions.md +1 -1
  96. package/skills/prototype/SKILL.md +4 -4
  97. package/skills/prototype/reference/honest-limitations.md +4 -4
  98. package/skills/prototype/reference/promotion-tasks.md +2 -2
  99. package/skills/prototype/workflows/promotion.md +4 -6
  100. package/skills/reviews/SKILL.md +85 -0
  101. package/skills/small-cl/SKILL.md +52 -0
  102. package/agents/code-verifier.md +0 -74
  103. package/docs/PR_DESCRIPTION_GUIDE.md +0 -157
  104. package/docs/references/code-review-enforcement.md +0 -132
  105. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +0 -53
  106. package/hooks/blocking/code_review_gate_deny.py +0 -74
  107. package/hooks/blocking/code_review_pr_create_gate.py +0 -198
  108. package/hooks/blocking/code_review_push_gate.py +0 -145
  109. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +0 -348
  110. package/hooks/blocking/code_review_stamp_store.py +0 -233
  111. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +0 -7
  112. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +0 -15
  113. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +0 -212
  114. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +0 -138
  115. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +0 -49
  116. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +0 -38
  117. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +0 -483
  118. package/hooks/blocking/config/code_review_enforcement_constants.py +0 -143
  119. package/hooks/blocking/config/test_code_review_enforcement_constants.py +0 -166
  120. package/hooks/blocking/config/verified_commit_constants.py +0 -160
  121. package/hooks/blocking/config/verified_commit_context_constants.py +0 -21
  122. package/hooks/blocking/config/verified_commit_gate_output_constants.py +0 -14
  123. package/hooks/blocking/conftest.py +0 -123
  124. package/hooks/blocking/convergence_gate_blocker.py +0 -310
  125. package/hooks/blocking/pr_converge_bugteam_enforcer.py +0 -170
  126. package/hooks/blocking/pr_description_body_audit.py +0 -148
  127. package/hooks/blocking/pr_description_command_parser.py +0 -237
  128. package/hooks/blocking/pr_description_enforcer.py +0 -227
  129. package/hooks/blocking/pr_description_pr_number.py +0 -155
  130. package/hooks/blocking/pr_description_proof_of_work.py +0 -385
  131. package/hooks/blocking/pr_description_readability.py +0 -364
  132. package/hooks/blocking/reviewer_spawn_gate.py +0 -182
  133. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +0 -62
  134. package/hooks/blocking/test_code_review_gate_deny.py +0 -54
  135. package/hooks/blocking/test_code_review_pr_create_gate.py +0 -199
  136. package/hooks/blocking/test_code_review_push_gate.py +0 -205
  137. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +0 -199
  138. package/hooks/blocking/test_code_review_stamp_store.py +0 -205
  139. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +0 -710
  140. package/hooks/blocking/test_code_verifier_tools_contract.py +0 -28
  141. package/hooks/blocking/test_convergence_gate_blocker.py +0 -428
  142. package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +0 -311
  143. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +0 -184
  144. package/hooks/blocking/test_pr_description_enforcer.py +0 -221
  145. package/hooks/blocking/test_pr_description_enforcer_body_audit.py +0 -247
  146. package/hooks/blocking/test_pr_description_enforcer_body_rules.py +0 -493
  147. package/hooks/blocking/test_pr_description_enforcer_command_parser.py +0 -366
  148. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +0 -159
  149. package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +0 -175
  150. package/hooks/blocking/test_pr_description_enforcer_readability.py +0 -443
  151. package/hooks/blocking/test_pr_description_proof_of_work.py +0 -313
  152. package/hooks/blocking/test_reviewer_spawn_gate.py +0 -230
  153. package/hooks/blocking/test_verdict_directory_write_blocker.py +0 -804
  154. package/hooks/blocking/test_verification_verdict_store.py +0 -974
  155. package/hooks/blocking/test_verified_commit_config_bootstrap.py +0 -67
  156. package/hooks/blocking/test_verified_commit_docs_delta.py +0 -176
  157. package/hooks/blocking/test_verified_commit_gate.py +0 -581
  158. package/hooks/blocking/test_verified_commit_gate_additional_context.py +0 -134
  159. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +0 -131
  160. package/hooks/blocking/test_verifier_verdict_minter.py +0 -299
  161. package/hooks/blocking/tests/test_verified_commit_gate.py +0 -41
  162. package/hooks/blocking/verdict_directory_write_blocker.py +0 -687
  163. package/hooks/blocking/verification_verdict_store.py +0 -1039
  164. package/hooks/blocking/verified_commit_config_bootstrap.py +0 -63
  165. package/hooks/blocking/verified_commit_gate.py +0 -113
  166. package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +0 -28
  167. package/hooks/blocking/verified_commit_gate_parts/__init__.py +0 -1
  168. package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +0 -174
  169. package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +0 -53
  170. package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +0 -80
  171. package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +0 -170
  172. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +0 -217
  173. package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +0 -10
  174. package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +0 -94
  175. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +0 -17
  176. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +0 -38
  177. package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +0 -71
  178. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +0 -96
  179. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +0 -167
  180. package/hooks/blocking/verifier_verdict_minter.py +0 -280
  181. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +0 -64
  182. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +0 -53
  183. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_constants.py +0 -55
  184. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_state.py +0 -67
  185. package/hooks/hooks_constants/pr_description_enforcer_constants.py +0 -167
  186. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -107
  187. package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +0 -41
  188. package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +0 -292
  189. package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +0 -198
  190. package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +0 -283
  191. package/rules/proof-of-work-pr-comments.md +0 -22
  192. package/rules/verified-commit-gate-skip.md +0 -4
  193. package/scripts/Show-Asset.ps1 +0 -106
@@ -1,345 +1,17 @@
1
- """Tests for hook_log_stop_wrapper -- debounced, fire-and-forget Stop hook."""
1
+ """Tests for the disabled hook_log_stop_wrapper Stop hook."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import os
6
- import subprocess
7
5
  import sys
8
- import time
9
6
  from pathlib import Path
10
7
 
11
- import pytest
12
-
13
8
  _HOOKS_ROOT = Path(__file__).resolve().parent.parent
14
9
  if str(_HOOKS_ROOT) not in sys.path:
15
10
  sys.path.insert(0, str(_HOOKS_ROOT))
16
11
 
17
12
  from diagnostic import hook_log_stop_wrapper
18
- from hooks_constants.hook_log_extractor_constants import (
19
- BWS_ACCESS_TOKEN_ENV_VAR,
20
- BWS_EXECUTABLE_NAME,
21
- FLAG_INCREMENTAL,
22
- STOP_WRAPPER_DEBOUNCE_SECONDS,
23
- )
24
-
25
-
26
- def _redirect_timestamp_path(
27
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
28
- ) -> Path:
29
- timestamp_file = tmp_path / "stop_wrapper_last_run.txt"
30
- monkeypatch.setattr(
31
- hook_log_stop_wrapper,
32
- "_last_run_timestamp_path",
33
- lambda: timestamp_file,
34
- )
35
- return timestamp_file
36
-
37
-
38
- def test_main_returns_zero_when_extractor_spawn_raises(
39
- monkeypatch: pytest.MonkeyPatch,
40
- tmp_path: Path,
41
- ) -> None:
42
- timestamp_file = _redirect_timestamp_path(monkeypatch, tmp_path)
43
- monkeypatch.delenv(BWS_ACCESS_TOKEN_ENV_VAR, raising=False)
44
- monkeypatch.setattr(hook_log_stop_wrapper.shutil, "which", lambda _name: None)
45
-
46
- def _raise(*_args: object, **_kwargs: object) -> None:
47
- raise RuntimeError("boom")
48
13
 
49
- monkeypatch.setattr(hook_log_stop_wrapper.subprocess, "Popen", _raise)
50
14
 
15
+ def test_main_returns_zero_without_side_effects() -> None:
16
+ """Disabled main exits success and does no work."""
51
17
  assert hook_log_stop_wrapper.main() == 0
52
- assert not timestamp_file.exists()
53
-
54
-
55
- def test_main_writes_timestamp_before_spawn_to_narrow_toctou_window(
56
- monkeypatch: pytest.MonkeyPatch,
57
- tmp_path: Path,
58
- ) -> None:
59
- timestamp_file = _redirect_timestamp_path(monkeypatch, tmp_path)
60
- monkeypatch.delenv(BWS_ACCESS_TOKEN_ENV_VAR, raising=False)
61
- monkeypatch.setattr(hook_log_stop_wrapper.shutil, "which", lambda _name: None)
62
-
63
- timestamp_file_existed_at_spawn_time: list[bool] = []
64
-
65
- def _capture_timestamp_state(*_args: object, **_kwargs: object) -> object:
66
- timestamp_file_existed_at_spawn_time.append(timestamp_file.exists())
67
- return object()
68
-
69
- monkeypatch.setattr(
70
- hook_log_stop_wrapper.subprocess, "Popen", _capture_timestamp_state
71
- )
72
-
73
- hook_log_stop_wrapper.main()
74
-
75
- assert timestamp_file_existed_at_spawn_time == [True]
76
-
77
-
78
- def test_main_removes_timestamp_when_spawn_fails_to_allow_retry(
79
- monkeypatch: pytest.MonkeyPatch,
80
- tmp_path: Path,
81
- ) -> None:
82
- timestamp_file = _redirect_timestamp_path(monkeypatch, tmp_path)
83
- timestamp_file.parent.mkdir(parents=True, exist_ok=True)
84
- stale_timestamp = time.time() - STOP_WRAPPER_DEBOUNCE_SECONDS - 1
85
- timestamp_file.write_text(str(stale_timestamp))
86
-
87
- monkeypatch.delenv(BWS_ACCESS_TOKEN_ENV_VAR, raising=False)
88
- monkeypatch.setattr(hook_log_stop_wrapper.shutil, "which", lambda _name: None)
89
-
90
- def _raise(*_args: object, **_kwargs: object) -> None:
91
- raise RuntimeError("boom")
92
-
93
- monkeypatch.setattr(hook_log_stop_wrapper.subprocess, "Popen", _raise)
94
-
95
- assert hook_log_stop_wrapper.main() == 0
96
- assert not timestamp_file.exists()
97
-
98
-
99
- def test_main_uses_bws_when_token_and_binary_present(
100
- monkeypatch: pytest.MonkeyPatch,
101
- tmp_path: Path,
102
- ) -> None:
103
- _redirect_timestamp_path(monkeypatch, tmp_path)
104
- monkeypatch.setenv(BWS_ACCESS_TOKEN_ENV_VAR, "secret-value")
105
- monkeypatch.setattr(
106
- hook_log_stop_wrapper.shutil,
107
- "which",
108
- lambda _name: "/usr/local/bin/bws",
109
- )
110
-
111
- captured_commands: list[list[str]] = []
112
-
113
- def _fake_popen(command_list: list[str], **_kwargs: object) -> object:
114
- captured_commands.append(list(command_list))
115
- return object()
116
-
117
- monkeypatch.setattr(hook_log_stop_wrapper.subprocess, "Popen", _fake_popen)
118
-
119
- exit_code = hook_log_stop_wrapper.main()
120
-
121
- assert exit_code == 0
122
- assert len(captured_commands) == 1
123
- assert captured_commands[0][0] == BWS_EXECUTABLE_NAME
124
- assert FLAG_INCREMENTAL in captured_commands[0]
125
-
126
-
127
- def test_main_skips_bws_when_token_missing(
128
- monkeypatch: pytest.MonkeyPatch,
129
- tmp_path: Path,
130
- ) -> None:
131
- _redirect_timestamp_path(monkeypatch, tmp_path)
132
- monkeypatch.delenv(BWS_ACCESS_TOKEN_ENV_VAR, raising=False)
133
- monkeypatch.setattr(
134
- hook_log_stop_wrapper.shutil,
135
- "which",
136
- lambda _name: "/usr/local/bin/bws",
137
- )
138
-
139
- captured_commands: list[list[str]] = []
140
-
141
- def _fake_popen(command_list: list[str], **_kwargs: object) -> object:
142
- captured_commands.append(list(command_list))
143
- return object()
144
-
145
- monkeypatch.setattr(hook_log_stop_wrapper.subprocess, "Popen", _fake_popen)
146
-
147
- exit_code = hook_log_stop_wrapper.main()
148
-
149
- assert exit_code == 0
150
- assert len(captured_commands) == 1
151
- assert BWS_EXECUTABLE_NAME not in captured_commands[0]
152
-
153
-
154
- def test_main_skips_bws_when_binary_missing(
155
- monkeypatch: pytest.MonkeyPatch,
156
- tmp_path: Path,
157
- ) -> None:
158
- _redirect_timestamp_path(monkeypatch, tmp_path)
159
- monkeypatch.setenv(BWS_ACCESS_TOKEN_ENV_VAR, "secret-value")
160
- monkeypatch.setattr(hook_log_stop_wrapper.shutil, "which", lambda _name: None)
161
-
162
- captured_commands: list[list[str]] = []
163
-
164
- def _fake_popen(command_list: list[str], **_kwargs: object) -> object:
165
- captured_commands.append(list(command_list))
166
- return object()
167
-
168
- monkeypatch.setattr(hook_log_stop_wrapper.subprocess, "Popen", _fake_popen)
169
-
170
- exit_code = hook_log_stop_wrapper.main()
171
-
172
- assert exit_code == 0
173
- assert len(captured_commands) == 1
174
- assert BWS_EXECUTABLE_NAME not in captured_commands[0]
175
-
176
-
177
- def test_main_skips_spawn_when_recent_timestamp_within_debounce_window(
178
- monkeypatch: pytest.MonkeyPatch,
179
- tmp_path: Path,
180
- ) -> None:
181
- timestamp_file = _redirect_timestamp_path(monkeypatch, tmp_path)
182
- timestamp_file.parent.mkdir(parents=True, exist_ok=True)
183
- timestamp_file.write_text(str(time.time()))
184
-
185
- captured_commands: list[list[str]] = []
186
-
187
- def _fake_popen(command_list: list[str], **_kwargs: object) -> object:
188
- captured_commands.append(list(command_list))
189
- return object()
190
-
191
- monkeypatch.setattr(hook_log_stop_wrapper.subprocess, "Popen", _fake_popen)
192
-
193
- exit_code = hook_log_stop_wrapper.main()
194
-
195
- assert exit_code == 0
196
- assert len(captured_commands) == 0
197
-
198
-
199
- def test_main_spawns_when_timestamp_older_than_debounce_window(
200
- monkeypatch: pytest.MonkeyPatch,
201
- tmp_path: Path,
202
- ) -> None:
203
- timestamp_file = _redirect_timestamp_path(monkeypatch, tmp_path)
204
- timestamp_file.parent.mkdir(parents=True, exist_ok=True)
205
- stale_timestamp = time.time() - STOP_WRAPPER_DEBOUNCE_SECONDS - 1
206
- timestamp_file.write_text(str(stale_timestamp))
207
-
208
- monkeypatch.delenv(BWS_ACCESS_TOKEN_ENV_VAR, raising=False)
209
- monkeypatch.setattr(hook_log_stop_wrapper.shutil, "which", lambda _name: None)
210
-
211
- captured_commands: list[list[str]] = []
212
-
213
- def _fake_popen(command_list: list[str], **_kwargs: object) -> object:
214
- captured_commands.append(list(command_list))
215
- return object()
216
-
217
- monkeypatch.setattr(hook_log_stop_wrapper.subprocess, "Popen", _fake_popen)
218
-
219
- exit_code = hook_log_stop_wrapper.main()
220
-
221
- assert exit_code == 0
222
- assert len(captured_commands) == 1
223
-
224
-
225
- def test_main_writes_current_timestamp_to_file(
226
- monkeypatch: pytest.MonkeyPatch,
227
- tmp_path: Path,
228
- ) -> None:
229
- timestamp_file = _redirect_timestamp_path(monkeypatch, tmp_path)
230
- monkeypatch.delenv(BWS_ACCESS_TOKEN_ENV_VAR, raising=False)
231
- monkeypatch.setattr(hook_log_stop_wrapper.shutil, "which", lambda _name: None)
232
- monkeypatch.setattr(
233
- hook_log_stop_wrapper.subprocess,
234
- "Popen",
235
- lambda *_args, **_kwargs: object(),
236
- )
237
-
238
- timestamp_before_call = time.time()
239
- hook_log_stop_wrapper.main()
240
- timestamp_after_call = time.time()
241
-
242
- assert timestamp_file.exists()
243
- written_timestamp = float(timestamp_file.read_text().strip())
244
- assert timestamp_before_call <= written_timestamp <= timestamp_after_call
245
-
246
-
247
- def test_main_passes_devnull_streams_to_detached_spawn(
248
- monkeypatch: pytest.MonkeyPatch,
249
- tmp_path: Path,
250
- ) -> None:
251
- _redirect_timestamp_path(monkeypatch, tmp_path)
252
- monkeypatch.delenv(BWS_ACCESS_TOKEN_ENV_VAR, raising=False)
253
- monkeypatch.setattr(hook_log_stop_wrapper.shutil, "which", lambda _name: None)
254
-
255
- captured_kwargs: dict[str, object] = {}
256
-
257
- def _fake_popen(command_list: list[str], **kwargs: object) -> object:
258
- captured_kwargs.update(kwargs)
259
- return object()
260
-
261
- monkeypatch.setattr(hook_log_stop_wrapper.subprocess, "Popen", _fake_popen)
262
-
263
- hook_log_stop_wrapper.main()
264
-
265
- assert captured_kwargs.get("stdin") is hook_log_stop_wrapper.subprocess.DEVNULL
266
- assert captured_kwargs.get("stdout") is hook_log_stop_wrapper.subprocess.DEVNULL
267
- assert captured_kwargs.get("stderr") is hook_log_stop_wrapper.subprocess.DEVNULL
268
-
269
-
270
- def test_main_recovers_when_timestamp_file_is_corrupted(
271
- monkeypatch: pytest.MonkeyPatch,
272
- tmp_path: Path,
273
- ) -> None:
274
- timestamp_file = _redirect_timestamp_path(monkeypatch, tmp_path)
275
- timestamp_file.parent.mkdir(parents=True, exist_ok=True)
276
- timestamp_file.write_text("not-a-float")
277
-
278
- monkeypatch.delenv(BWS_ACCESS_TOKEN_ENV_VAR, raising=False)
279
- monkeypatch.setattr(hook_log_stop_wrapper.shutil, "which", lambda _name: None)
280
-
281
- captured_commands: list[list[str]] = []
282
-
283
- def _fake_popen(command_list: list[str], **_kwargs: object) -> object:
284
- captured_commands.append(list(command_list))
285
- return object()
286
-
287
- monkeypatch.setattr(hook_log_stop_wrapper.subprocess, "Popen", _fake_popen)
288
-
289
- exit_code = hook_log_stop_wrapper.main()
290
-
291
- assert exit_code == 0
292
- assert len(captured_commands) == 1
293
-
294
-
295
- def test_main_treats_future_timestamp_as_not_debounced(
296
- monkeypatch: pytest.MonkeyPatch,
297
- tmp_path: Path,
298
- ) -> None:
299
- timestamp_file = _redirect_timestamp_path(monkeypatch, tmp_path)
300
- timestamp_file.parent.mkdir(parents=True, exist_ok=True)
301
- far_future_timestamp = time.time() + 3600
302
- timestamp_file.write_text(str(far_future_timestamp))
303
-
304
- monkeypatch.delenv(BWS_ACCESS_TOKEN_ENV_VAR, raising=False)
305
- monkeypatch.setattr(hook_log_stop_wrapper.shutil, "which", lambda _name: None)
306
-
307
- captured_commands: list[list[str]] = []
308
-
309
- def _fake_popen(command_list: list[str], **_kwargs: object) -> object:
310
- captured_commands.append(list(command_list))
311
- return object()
312
-
313
- monkeypatch.setattr(hook_log_stop_wrapper.subprocess, "Popen", _fake_popen)
314
-
315
- exit_code = hook_log_stop_wrapper.main()
316
-
317
- assert exit_code == 0
318
- assert len(captured_commands) == 1
319
-
320
-
321
- def test_main_passes_hidden_startupinfo_on_windows(
322
- monkeypatch: pytest.MonkeyPatch,
323
- tmp_path: Path,
324
- ) -> None:
325
- _redirect_timestamp_path(monkeypatch, tmp_path)
326
- monkeypatch.delenv(BWS_ACCESS_TOKEN_ENV_VAR, raising=False)
327
- monkeypatch.setattr(hook_log_stop_wrapper.shutil, "which", lambda _name: None)
328
-
329
- captured_kwargs: dict[str, object] = {}
330
-
331
- def _fake_popen(command_list: list[str], **kwargs: object) -> object:
332
- captured_kwargs.update(kwargs)
333
- return object()
334
-
335
- monkeypatch.setattr(hook_log_stop_wrapper.subprocess, "Popen", _fake_popen)
336
-
337
- hook_log_stop_wrapper.main()
338
-
339
- if os.name == "nt":
340
- startup_info = captured_kwargs.get("startupinfo")
341
- assert startup_info is not None
342
- assert startup_info.dwFlags & subprocess.STARTF_USESHOWWINDOW
343
- assert startup_info.wShowWindow == subprocess.SW_HIDE
344
- else:
345
- assert captured_kwargs.get("start_new_session") is True
@@ -76,12 +76,6 @@ ALL_GIT_VERIFY_REFERENCE_COMMAND_PREFIX: tuple[str, ...] = (
76
76
  )
77
77
  ALL_GIT_MERGE_BASE_COMMAND_PREFIX: tuple[str, ...] = ("git", "merge-base")
78
78
  GIT_REFERENCE_QUERY_TIMEOUT_SECONDS: int = 15
79
- CODE_REVIEW_PUSH_GATE_PATH_OVERRIDE_ENV_VAR: str = "CODE_REVIEW_PUSH_GATE_PATH"
80
- CODE_REVIEW_PUSH_GATE_MODULE_NAME: str = "code_review_push_gate"
81
- CODE_REVIEW_PUSH_GATE_SCRIPT_FILENAME: str = "code_review_push_gate.py"
82
- CODE_REVIEW_DENY_REASON_FUNCTION_NAME: str = "deny_reason_for_directory"
83
- BLOCKING_DIRECTORY_NAME: str = "blocking"
84
- CODE_REVIEW_STAMP_BLOCK_EXIT_CODE: int = 1
85
79
  ALL_PROTECTED_BRANCH_PUSH_NAMES: tuple[str, ...] = ("main", "master")
86
80
  PROTECTED_BRANCH_PUSH_BLOCK_EXIT_CODE: int = 1
87
81
  PROTECTED_BRANCH_PUSH_BLOCK_MESSAGE: str = (
@@ -44,8 +44,6 @@ Exit codes:
44
44
 
45
45
  from __future__ import annotations
46
46
 
47
- import importlib.util
48
- import os
49
47
  import subprocess
50
48
  import sys
51
49
  from pathlib import Path
@@ -59,12 +57,6 @@ from git_hooks_constants import (
59
57
  ALL_PROTECTED_BRANCH_PUSH_NAMES,
60
58
  ALL_ZEROS_OBJECT_NAME_CHARACTER,
61
59
  BASE_REFERENCE_ARGUMENT,
62
- BLOCKING_DIRECTORY_NAME,
63
- CODE_REVIEW_DENY_REASON_FUNCTION_NAME,
64
- CODE_REVIEW_PUSH_GATE_MODULE_NAME,
65
- CODE_REVIEW_PUSH_GATE_PATH_OVERRIDE_ENV_VAR,
66
- CODE_REVIEW_PUSH_GATE_SCRIPT_FILENAME,
67
- CODE_REVIEW_STAMP_BLOCK_EXIT_CODE,
68
60
  DEFAULT_REMOTE_BASE_REFERENCE,
69
61
  GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE,
70
62
  GIT_COMMAND_TIMEOUT_SECONDS,
@@ -509,80 +501,6 @@ def invoke_gate(gate_script_path: Path, base_reference: str) -> int:
509
501
  return completion.returncode
510
502
 
511
503
 
512
- def resolve_code_review_gate_path() -> tuple[Path, Path | None]:
513
- """Return the code-review push gate script path and its exact-allow override.
514
-
515
- The override environment variable, when set, names the only path the trust
516
- check accepts; otherwise the gate script resolves to the ``blocking``
517
- directory beside this hook's ``git-hooks`` directory, which holds in both
518
- the repository layout and the installed ``~/.claude/hooks`` layout.
519
-
520
- Returns:
521
- A pair of the resolved gate script path and either the exact override
522
- path (override set) or None (trust-root case).
523
- """
524
- override_path_raw = os.environ.get(
525
- CODE_REVIEW_PUSH_GATE_PATH_OVERRIDE_ENV_VAR, ""
526
- ).strip()
527
- if override_path_raw:
528
- exact_override = Path(override_path_raw).resolve()
529
- return exact_override, exact_override
530
- blocking_directory = Path(__file__).resolve().parent.parent / BLOCKING_DIRECTORY_NAME
531
- return blocking_directory / CODE_REVIEW_PUSH_GATE_SCRIPT_FILENAME, None
532
-
533
-
534
- def load_code_review_deny_reason(gate_script_path: Path, work_tree_directory: str) -> str | None:
535
- """Import the gate script and read its push deny reason for a work tree.
536
-
537
- Any import or evaluation failure reads as no deny reason.
538
-
539
- Args:
540
- gate_script_path: The resolved code-review push gate script path.
541
- work_tree_directory: The work tree the push targets.
542
-
543
- Returns:
544
- The gate's deny reason when the surface lacks a covering low stamp;
545
- None when the gate allows the push or could not be evaluated.
546
- """
547
- try:
548
- gate_specification = importlib.util.spec_from_file_location(
549
- CODE_REVIEW_PUSH_GATE_MODULE_NAME, str(gate_script_path)
550
- )
551
- if gate_specification is None or gate_specification.loader is None:
552
- return None
553
- gate_module = importlib.util.module_from_spec(gate_specification)
554
- gate_specification.loader.exec_module(gate_module)
555
- deny_reason_function = getattr(gate_module, CODE_REVIEW_DENY_REASON_FUNCTION_NAME, None)
556
- if deny_reason_function is None:
557
- return None
558
- deny_reason = deny_reason_function(work_tree_directory)
559
- except Exception:
560
- return None
561
- if isinstance(deny_reason, str) and deny_reason:
562
- return deny_reason
563
- return None
564
-
565
-
566
- def code_review_stamp_block_exit_code() -> int:
567
- """Block the push when no clean low code-review stamp covers the surface.
568
-
569
- Runs the gate decision only when the gate script sits at a trusted
570
- installed location (or the exact override path). When the gate is absent
571
- from a trusted location, the check is skipped and the push is allowed,
572
- matching the CODE_RULES gate's fail-open posture.
573
-
574
- Returns:
575
- The block exit code when a covering low stamp is missing; 0 when the
576
- surface is covered or the gate is not installed at a trusted location.
577
- """
578
- gate_script_path, exact_allowed_path = resolve_code_review_gate_path()
579
- if not is_safe_regular_file(gate_script_path, exact_allowed_path):
580
- return 0
581
- deny_reason = load_code_review_deny_reason(gate_script_path, os.getcwd())
582
- if deny_reason is None:
583
- return 0
584
- sys.stderr.write(deny_reason + "\n")
585
- return CODE_REVIEW_STAMP_BLOCK_EXIT_CODE
586
504
 
587
505
 
588
506
  def main() -> int:
@@ -623,7 +541,7 @@ def main() -> int:
623
541
  pre_push_gate_script_not_found_message.format(path=gate_script_path),
624
542
  file=sys.stderr,
625
543
  )
626
- return code_review_stamp_block_exit_code()
544
+ return 0
627
545
  base_reference = _resolve_gate_base_from_parsed(parsed_stdin)
628
546
  if base_reference is None:
629
547
  return 0
@@ -632,7 +550,7 @@ def main() -> int:
632
550
  return gate_infrastructure_failure_exit_code
633
551
  if base_reference == unresolvable_merge_base_sentinel:
634
552
  print(unresolvable_merge_base_message, file=sys.stderr)
635
- return code_review_stamp_block_exit_code()
553
+ return 0
636
554
  remote_name = resolve_remote_name_from_arguments(sys.argv)
637
555
  try:
638
556
  usable_base_reference = resolve_usable_base_reference(
@@ -645,7 +563,7 @@ def main() -> int:
645
563
  code_rules_exit_code = invoke_gate(gate_script_path, usable_base_reference)
646
564
  if code_rules_exit_code != 0:
647
565
  return code_rules_exit_code
648
- return code_review_stamp_block_exit_code()
566
+ return 0
649
567
 
650
568
 
651
569
  if __name__ == "__main__":