claude-dev-env 2.11.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 (203) hide show
  1. package/_shared/advisor/reference/sol-rung.md +5 -2
  2. package/_shared/advisor/reference/warm-up.md +1 -0
  3. package/_shared/advisor/scripts/codex_sol_advisor.py +96 -31
  4. package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +5 -0
  5. package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +173 -11
  6. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  7. package/_shared/pr-loop/scripts/test_code_rules_gate.py +0 -26
  8. package/agents/CLAUDE.md +1 -2
  9. package/agents/pr-description-writer.md +11 -81
  10. package/bin/install.mjs +48 -9
  11. package/bin/install.prune.test.mjs +26 -0
  12. package/bin/install.settings-defaults.test.mjs +60 -33
  13. package/bin/install.test.mjs +2 -4
  14. package/commands/CLAUDE.md +1 -0
  15. package/commands/sr-loop.md +52 -0
  16. package/docs/CLAUDE.md +0 -1
  17. package/docs/references/CLAUDE.md +0 -1
  18. package/hooks/CLAUDE.md +1 -1
  19. package/hooks/blocking/CLAUDE.md +2 -27
  20. package/hooks/blocking/config/CLAUDE.md +1 -13
  21. package/hooks/blocking/conventional_pr_title_gate.py +1 -2
  22. package/hooks/blocking/send_user_file_open_locally_blocker.py +1 -1
  23. package/hooks/blocking/stop_dispatcher.py +4 -4
  24. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -3
  25. package/hooks/blocking/test_pre_tool_use_dispatcher.py +6 -7
  26. package/hooks/blocking/test_send_user_file_open_locally_blocker.py +4 -3
  27. package/hooks/blocking/test_shared_stdin_adoption.py +0 -2
  28. package/hooks/diagnostic/CLAUDE.md +3 -3
  29. package/hooks/diagnostic/hook_log_extractor.py +2 -36
  30. package/hooks/diagnostic/hook_log_stop_wrapper.py +6 -155
  31. package/hooks/diagnostic/test_hook_log_extractor.py +8 -21
  32. package/hooks/diagnostic/test_hook_log_stop_wrapper.py +3 -331
  33. package/hooks/git-hooks/git_hooks_constants/__init__.py +0 -6
  34. package/hooks/git-hooks/pre_push.py +3 -85
  35. package/hooks/git-hooks/test_pre_push.py +0 -130
  36. package/hooks/hooks.json +0 -35
  37. package/hooks/hooks_constants/CLAUDE.md +0 -9
  38. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -11
  39. package/hooks/hooks_constants/convergence_branch_constants.py +0 -1
  40. package/hooks/hooks_constants/fable_spawn_gate_constants.py +3 -4
  41. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -8
  42. package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +1 -6
  43. package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -1
  44. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -26
  45. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +0 -5
  46. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -1
  47. package/hooks/lifecycle/CLAUDE.md +1 -3
  48. package/package.json +1 -1
  49. package/rules/CLAUDE.md +0 -2
  50. package/rules/gh-cli-conventions.md +0 -1
  51. package/rules/git-workflow.md +1 -3
  52. package/scripts/CLAUDE.md +0 -1
  53. package/scripts/_code_review_test_support.py +0 -95
  54. package/scripts/dev_env_scripts_constants/code_review_constants.py +9 -98
  55. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +1 -44
  56. package/scripts/invoke_code_review.py +8 -451
  57. package/scripts/test_invoke_code_review.py +5 -61
  58. package/scripts/test_invoke_code_review_cli.py +1 -45
  59. package/scripts/test_invoke_code_review_contract.py +1 -118
  60. package/settings.json +1 -10
  61. package/skills/CLAUDE.md +8 -0
  62. package/skills/autoconverge/reference/CLAUDE.md +1 -1
  63. package/skills/autoconverge/reference/convergence.md +7 -7
  64. package/skills/autoconverge/reference/gotchas.md +3 -10
  65. package/skills/autoconverge/workflow/converge.contract.test.mjs +53 -1388
  66. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +4 -3
  67. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +33 -493
  68. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +15 -97
  69. package/skills/autoconverge/workflow/converge.mjs +201 -238
  70. package/skills/autoconverge/workflow/converge.precatch.test.mjs +1 -1
  71. package/skills/bugteam/CONSTRAINTS.md +1 -1
  72. package/skills/bugteam/SKILL.md +9 -0
  73. package/skills/bugteam/reference/audit-and-teammates.md +10 -6
  74. package/skills/bugteam/test_skill_additions.py +9 -0
  75. package/skills/comments/SKILL.md +65 -0
  76. package/skills/descriptions/SKILL.md +51 -0
  77. package/skills/emergencies/SKILL.md +42 -0
  78. package/skills/imagegen/SKILL.md +21 -0
  79. package/skills/imagegen/scripts/config/__init__.py +1 -0
  80. package/skills/imagegen/scripts/config/constants.py +28 -0
  81. package/skills/imagegen/scripts/imagegen.py +45 -0
  82. package/skills/imagegen/scripts/imagegen_core.py +528 -0
  83. package/skills/imagegen/scripts/test_imagegen.py +204 -0
  84. package/skills/orchestrator/SKILL.md +2 -4
  85. package/skills/plan-to-pr/SKILL.md +6 -7
  86. package/skills/plan-to-pr/reference/final-validation-tasks.md +1 -1
  87. package/skills/plan-to-pr/reference/process-inventory.md +2 -2
  88. package/skills/plan-to-pr/reference/review-loop.md +2 -2
  89. package/skills/plan-to-pr/reference/run-record.schema.json +1 -1
  90. package/skills/plan-to-pr/reference/task-seeds.md +1 -1
  91. package/skills/plan-to-pr/reference/task-ticket.md +3 -4
  92. package/skills/plan-to-pr/scripts/config/constants.py +1 -2
  93. package/skills/plan-to-pr/scripts/test_validate_protocol.py +1 -1
  94. package/skills/plan-to-pr/test_skill_contract.py +2 -2
  95. package/skills/pr-converge/SKILL.md +13 -16
  96. package/skills/pr-converge/reference/progress-checklist.md +6 -5
  97. package/skills/pr-converge/reference/state-schema.md +1 -27
  98. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  99. package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +3 -3
  100. package/skills/pr-loop-lifecycle/SKILL.md +1 -1
  101. package/skills/pr-loop-lifecycle/reference/teardown-publish-permissions.md +1 -1
  102. package/skills/prototype/SKILL.md +4 -4
  103. package/skills/prototype/reference/honest-limitations.md +4 -4
  104. package/skills/prototype/reference/promotion-tasks.md +2 -2
  105. package/skills/prototype/workflows/promotion.md +4 -6
  106. package/skills/reviews/SKILL.md +85 -0
  107. package/skills/small-cl/SKILL.md +52 -0
  108. package/skills/usage-pause/SKILL.md +1 -1
  109. package/skills/usage-pause/scripts/resolve_usage_window.py +32 -4
  110. package/skills/usage-pause/scripts/test_resolve_usage_window.py +26 -0
  111. package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +3 -1
  112. package/agents/code-verifier.md +0 -74
  113. package/docs/PR_DESCRIPTION_GUIDE.md +0 -157
  114. package/docs/references/code-review-enforcement.md +0 -132
  115. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +0 -53
  116. package/hooks/blocking/code_review_gate_deny.py +0 -74
  117. package/hooks/blocking/code_review_pr_create_gate.py +0 -198
  118. package/hooks/blocking/code_review_push_gate.py +0 -145
  119. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +0 -348
  120. package/hooks/blocking/code_review_stamp_store.py +0 -233
  121. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +0 -7
  122. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +0 -15
  123. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +0 -212
  124. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +0 -138
  125. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +0 -49
  126. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +0 -38
  127. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +0 -483
  128. package/hooks/blocking/config/code_review_enforcement_constants.py +0 -143
  129. package/hooks/blocking/config/test_code_review_enforcement_constants.py +0 -166
  130. package/hooks/blocking/config/verified_commit_constants.py +0 -160
  131. package/hooks/blocking/config/verified_commit_context_constants.py +0 -21
  132. package/hooks/blocking/config/verified_commit_gate_output_constants.py +0 -14
  133. package/hooks/blocking/conftest.py +0 -123
  134. package/hooks/blocking/convergence_gate_blocker.py +0 -310
  135. package/hooks/blocking/pr_converge_bugteam_enforcer.py +0 -170
  136. package/hooks/blocking/pr_description_body_audit.py +0 -148
  137. package/hooks/blocking/pr_description_command_parser.py +0 -237
  138. package/hooks/blocking/pr_description_enforcer.py +0 -227
  139. package/hooks/blocking/pr_description_pr_number.py +0 -155
  140. package/hooks/blocking/pr_description_proof_of_work.py +0 -385
  141. package/hooks/blocking/pr_description_readability.py +0 -364
  142. package/hooks/blocking/reviewer_spawn_gate.py +0 -182
  143. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +0 -62
  144. package/hooks/blocking/test_code_review_gate_deny.py +0 -54
  145. package/hooks/blocking/test_code_review_pr_create_gate.py +0 -199
  146. package/hooks/blocking/test_code_review_push_gate.py +0 -205
  147. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +0 -199
  148. package/hooks/blocking/test_code_review_stamp_store.py +0 -205
  149. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +0 -710
  150. package/hooks/blocking/test_code_verifier_tools_contract.py +0 -28
  151. package/hooks/blocking/test_convergence_gate_blocker.py +0 -428
  152. package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +0 -311
  153. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +0 -184
  154. package/hooks/blocking/test_pr_description_enforcer.py +0 -221
  155. package/hooks/blocking/test_pr_description_enforcer_body_audit.py +0 -247
  156. package/hooks/blocking/test_pr_description_enforcer_body_rules.py +0 -493
  157. package/hooks/blocking/test_pr_description_enforcer_command_parser.py +0 -366
  158. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +0 -159
  159. package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +0 -175
  160. package/hooks/blocking/test_pr_description_enforcer_readability.py +0 -443
  161. package/hooks/blocking/test_pr_description_proof_of_work.py +0 -313
  162. package/hooks/blocking/test_reviewer_spawn_gate.py +0 -230
  163. package/hooks/blocking/test_verdict_directory_write_blocker.py +0 -804
  164. package/hooks/blocking/test_verification_verdict_store.py +0 -974
  165. package/hooks/blocking/test_verified_commit_config_bootstrap.py +0 -67
  166. package/hooks/blocking/test_verified_commit_docs_delta.py +0 -176
  167. package/hooks/blocking/test_verified_commit_gate.py +0 -581
  168. package/hooks/blocking/test_verified_commit_gate_additional_context.py +0 -134
  169. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +0 -131
  170. package/hooks/blocking/test_verifier_verdict_minter.py +0 -299
  171. package/hooks/blocking/tests/test_verified_commit_gate.py +0 -41
  172. package/hooks/blocking/verdict_directory_write_blocker.py +0 -687
  173. package/hooks/blocking/verification_verdict_store.py +0 -1039
  174. package/hooks/blocking/verified_commit_config_bootstrap.py +0 -63
  175. package/hooks/blocking/verified_commit_gate.py +0 -113
  176. package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +0 -28
  177. package/hooks/blocking/verified_commit_gate_parts/__init__.py +0 -1
  178. package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +0 -174
  179. package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +0 -53
  180. package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +0 -80
  181. package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +0 -170
  182. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +0 -217
  183. package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +0 -10
  184. package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +0 -94
  185. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +0 -17
  186. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +0 -38
  187. package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +0 -71
  188. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +0 -96
  189. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +0 -167
  190. package/hooks/blocking/verifier_verdict_minter.py +0 -280
  191. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +0 -64
  192. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +0 -53
  193. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_constants.py +0 -55
  194. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_state.py +0 -67
  195. package/hooks/hooks_constants/pr_description_enforcer_constants.py +0 -167
  196. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -107
  197. package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +0 -41
  198. package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +0 -292
  199. package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +0 -198
  200. package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +0 -283
  201. package/rules/proof-of-work-pr-comments.md +0 -22
  202. package/rules/verified-commit-gate-skip.md +0 -4
  203. 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__":