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,710 +0,0 @@
1
- """Behavior tests for the code_verifier_spawn_preflight_gate PreToolUse hook.
2
-
3
- Each test builds a real git repository in a temporary directory, writes real
4
- files (real merge conflicts, real CODE_RULES violations), and runs the hook
5
- script as a subprocess with a real Agent PreToolUse JSON payload on stdin —
6
- the exact production invocation path. No mocks. The harness mirrors
7
- test_precommit_code_rules_gate.py lines 1-70.
8
- """
9
-
10
- import json
11
- import shutil
12
- import subprocess
13
- import sys
14
- from pathlib import Path
15
-
16
- HOOK_PATH = Path(__file__).resolve().parent / "code_verifier_spawn_preflight_gate.py"
17
-
18
- CLEAN_MODULE_SOURCE = '''"""Increment helper used by the preflight gate tests."""
19
-
20
-
21
- def add_one(number: int) -> int:
22
- """Return *number* plus one.
23
-
24
- Args:
25
- number: The integer to increment.
26
-
27
- Returns:
28
- The incremented integer.
29
- """
30
- return number + 1
31
- '''
32
-
33
- CLEAN_MODULE_SOURCE_EDITED = '''"""Increment helper used by the preflight gate tests."""
34
-
35
-
36
- def add_one(number: int) -> int:
37
- """Return *number* plus two.
38
-
39
- Args:
40
- number: The integer to increment.
41
-
42
- Returns:
43
- The incremented integer.
44
- """
45
- return number + 2
46
- '''
47
-
48
- VIOLATING_MODULE_SOURCE = '''"""Module carrying a banned identifier for the preflight gate tests."""
49
-
50
-
51
- def compute_total() -> int:
52
- """Return a fixed total.
53
-
54
- Returns:
55
- The fixed total.
56
- """
57
- result = 1
58
- return result
59
- '''
60
-
61
- CLEAN_TWO_FUNCTION_SOURCE = '''"""Two-function helper used by the preflight gate tests."""
62
-
63
-
64
- def add_one(number: int) -> int:
65
- """Return *number* plus one.
66
-
67
- Args:
68
- number: The integer to increment.
69
-
70
- Returns:
71
- The incremented integer.
72
- """
73
- return number + 1
74
-
75
-
76
- def add_two(number: int) -> int:
77
- """Return *number* plus two.
78
-
79
- Args:
80
- number: The integer to increment.
81
-
82
- Returns:
83
- The integer plus two.
84
- """
85
- return number + 2
86
- '''
87
-
88
- PREEXISTING_VIOLATION_BASE_SOURCE = '''"""Module that already carries a banned identifier at the base commit."""
89
-
90
-
91
- def compute_total() -> int:
92
- """Return a fixed total.
93
-
94
- Returns:
95
- The fixed total.
96
- """
97
- result = 1
98
- return result
99
-
100
-
101
- def add_one(number: int) -> int:
102
- """Return *number* plus one.
103
-
104
- Args:
105
- number: The integer to increment.
106
-
107
- Returns:
108
- The incremented integer.
109
- """
110
- return number + 1
111
- '''
112
-
113
- PREEXISTING_VIOLATION_EDITED_SOURCE = '''"""Module that already carries a banned identifier at the base commit."""
114
-
115
-
116
- def compute_total() -> int:
117
- """Return a fixed total.
118
-
119
- Returns:
120
- The fixed total.
121
- """
122
- result = 1
123
- return result
124
-
125
-
126
- def add_one(number: int) -> int:
127
- """Return *number* plus two.
128
-
129
- Args:
130
- number: The integer to increment.
131
-
132
- Returns:
133
- The integer plus two.
134
- """
135
- return number + 2
136
- '''
137
-
138
- SHARED_BASE_SOURCE = '''"""Shared module the conflict fixture edits on both sides."""
139
-
140
-
141
- def shared_value() -> int:
142
- """Return the shared base value.
143
-
144
- Returns:
145
- The shared integer.
146
- """
147
- return 100
148
- '''
149
-
150
- SHARED_FEATURE_SOURCE = '''"""Shared module the conflict fixture edits on both sides."""
151
-
152
-
153
- def shared_value() -> int:
154
- """Return the shared base value.
155
-
156
- Returns:
157
- The shared integer.
158
- """
159
- return 200
160
- '''
161
-
162
- SHARED_DIVERGENT_SOURCE = '''"""Shared module the conflict fixture edits on both sides."""
163
-
164
-
165
- def shared_value() -> int:
166
- """Return the shared base value.
167
-
168
- Returns:
169
- The shared integer.
170
- """
171
- return 300
172
- '''
173
-
174
- OTHER_DIVERGENT_SOURCE = '''"""Unrelated module the divergent base edits for the behind-but-clean case."""
175
-
176
-
177
- def other_value() -> int:
178
- """Return the unrelated value.
179
-
180
- Returns:
181
- The unrelated integer.
182
- """
183
- return 42
184
- '''
185
-
186
- BROKEN_ENFORCER_SOURCE = (
187
- '"""Enforcer stub that raises a non-import error at load time."""\n'
188
- "\n"
189
- "raise RuntimeError('deliberate enforcer load failure')\n"
190
- )
191
-
192
- RE_ERROR_ENFORCER_SOURCE = (
193
- '"""Enforcer stub whose module-scope regex compile raises re.error."""\n'
194
- "\n"
195
- "import re\n"
196
- "\n"
197
- "re.compile('(')\n"
198
- )
199
-
200
-
201
- def run_git(repository_root: Path, *git_arguments: str) -> str:
202
- completed = subprocess.run(
203
- ["git", "-C", str(repository_root), *git_arguments],
204
- check=True,
205
- capture_output=True,
206
- text=True,
207
- )
208
- return completed.stdout.strip()
209
-
210
-
211
- def disable_native_git_hooks(repository_root: Path) -> None:
212
- empty_hooks_directory = repository_root.parent / "nohooks"
213
- empty_hooks_directory.mkdir(exist_ok=True)
214
- run_git(repository_root, "config", "core.hooksPath", str(empty_hooks_directory))
215
-
216
-
217
- def initialize_repository(repository_root: Path) -> None:
218
- run_git(repository_root, "init")
219
- run_git(repository_root, "config", "user.email", "tests@example.com")
220
- run_git(repository_root, "config", "user.name", "Preflight Tests")
221
- disable_native_git_hooks(repository_root)
222
- run_git(repository_root, "checkout", "-b", "main")
223
- (repository_root / "base.py").write_text(CLEAN_MODULE_SOURCE, encoding="utf-8")
224
- run_git(repository_root, "add", "base.py")
225
- run_git(repository_root, "commit", "-m", "initial")
226
- main_sha = run_git(repository_root, "rev-parse", "HEAD")
227
- run_git(repository_root, "update-ref", "refs/remotes/origin/main", main_sha)
228
- resolved_base = run_git(repository_root, "merge-base", "HEAD", "origin/main")
229
- assert resolved_base, "fixture must resolve a merge base against origin/main"
230
-
231
-
232
- def commit_file(repository_root: Path, relative_name: str, source_text: str, message: str) -> str:
233
- (repository_root / relative_name).write_text(source_text, encoding="utf-8")
234
- run_git(repository_root, "add", relative_name)
235
- run_git(repository_root, "commit", "-m", message)
236
- return run_git(repository_root, "rev-parse", "HEAD")
237
-
238
-
239
- def write_working_tree_file(repository_root: Path, relative_name: str, source_text: str) -> None:
240
- (repository_root / relative_name).write_text(source_text, encoding="utf-8")
241
-
242
-
243
- def write_invalid_utf8_file(repository_root: Path, relative_name: str) -> None:
244
- (repository_root / relative_name).write_bytes(b"\xff\xfe invalid utf-8 bytes\n")
245
-
246
-
247
- def advance_origin_main_divergent(
248
- repository_root: Path, base_sha: str, relative_name: str, source_text: str
249
- ) -> None:
250
- run_git(repository_root, "checkout", "-b", "divergent", base_sha)
251
- commit_file(repository_root, relative_name, source_text, "divergent edit")
252
- divergent_sha = run_git(repository_root, "rev-parse", "HEAD")
253
- run_git(repository_root, "update-ref", "refs/remotes/origin/main", divergent_sha)
254
- run_git(repository_root, "checkout", "feature")
255
-
256
-
257
- def _spawn_payload(tool_name: str, subagent_type: str, prompt: str, cwd: Path) -> str:
258
- return json.dumps(
259
- {
260
- "tool_name": tool_name,
261
- "tool_input": {"subagent_type": subagent_type, "prompt": prompt},
262
- "cwd": str(cwd),
263
- }
264
- )
265
-
266
-
267
- def write_agent_payload(subagent_type: str, prompt: str, cwd: Path) -> str:
268
- return _spawn_payload("Agent", subagent_type, prompt, cwd)
269
-
270
-
271
- def write_task_payload(subagent_type: str, prompt: str, cwd: Path) -> str:
272
- return _spawn_payload("Task", subagent_type, prompt, cwd)
273
-
274
-
275
- def run_hook(payload: str, cwd: Path) -> subprocess.CompletedProcess[str]:
276
- return subprocess.run(
277
- [sys.executable, str(HOOK_PATH)],
278
- check=False,
279
- input=payload,
280
- capture_output=True,
281
- text=True,
282
- cwd=str(cwd),
283
- timeout=120,
284
- )
285
-
286
-
287
- def is_allow(result: subprocess.CompletedProcess[str]) -> bool:
288
- stdout_text = result.stdout.strip()
289
- if not stdout_text:
290
- return True
291
- parsed = json.loads(stdout_text)
292
- hook_output = parsed.get("hookSpecificOutput", {})
293
- return hook_output.get("permissionDecision") != "deny"
294
-
295
-
296
- def deny_reason(result: subprocess.CompletedProcess[str]) -> str:
297
- parsed = json.loads(result.stdout.strip())
298
- return parsed["hookSpecificOutput"]["permissionDecisionReason"]
299
-
300
-
301
- def make_conflict_repository(tmp_path: Path) -> Path:
302
- repository_root = tmp_path / "repo"
303
- repository_root.mkdir()
304
- initialize_repository(repository_root)
305
- base_sha = commit_file(repository_root, "shared.py", SHARED_BASE_SOURCE, "add shared")
306
- run_git(repository_root, "checkout", "-b", "feature")
307
- commit_file(repository_root, "shared.py", SHARED_FEATURE_SOURCE, "feature edit")
308
- advance_origin_main_divergent(repository_root, base_sha, "shared.py", SHARED_DIVERGENT_SOURCE)
309
- return repository_root
310
-
311
-
312
- def make_stale_self_upstream_conflict_repository(tmp_path: Path) -> Path:
313
- repository_root = make_conflict_repository(tmp_path)
314
- feature_sha = run_git(repository_root, "rev-parse", "HEAD")
315
- run_git(repository_root, "remote", "add", "origin", str(repository_root))
316
- run_git(repository_root, "update-ref", "refs/remotes/origin/feature", feature_sha)
317
- run_git(repository_root, "config", "branch.feature.remote", "origin")
318
- run_git(repository_root, "config", "branch.feature.merge", "refs/heads/feature")
319
- return repository_root
320
-
321
-
322
- def make_merge_in_progress_repository(tmp_path: Path) -> Path:
323
- repository_root = tmp_path / "repo"
324
- repository_root.mkdir()
325
- initialize_repository(repository_root)
326
- base_sha = commit_file(repository_root, "shared.py", SHARED_BASE_SOURCE, "add shared")
327
- run_git(repository_root, "update-ref", "refs/remotes/origin/main", base_sha)
328
- run_git(repository_root, "checkout", "-b", "feature")
329
- commit_file(repository_root, "shared.py", SHARED_FEATURE_SOURCE, "feature edit")
330
- advance_origin_main_divergent(repository_root, base_sha, "shared.py", SHARED_DIVERGENT_SOURCE)
331
- return repository_root
332
-
333
-
334
- def start_conflicting_merge(repository_root: Path) -> None:
335
- subprocess.run(
336
- ["git", "-C", str(repository_root), "merge", "--no-edit", "origin/main"],
337
- check=False,
338
- capture_output=True,
339
- text=True,
340
- )
341
-
342
-
343
- def test_non_code_verifier_agent_is_no_op(tmp_path: Path) -> None:
344
- repository_root = make_conflict_repository(tmp_path)
345
- write_working_tree_file(repository_root, "violator.py", VIOLATING_MODULE_SOURCE)
346
- payload = write_agent_payload("clean-coder", "do an audit", repository_root)
347
- result = run_hook(payload, repository_root)
348
- assert is_allow(result)
349
-
350
-
351
- def test_non_agent_tool_is_no_op(tmp_path: Path) -> None:
352
- repository_root = tmp_path / "repo"
353
- repository_root.mkdir()
354
- initialize_repository(repository_root)
355
- payload = json.dumps(
356
- {"tool_name": "Bash", "tool_input": {"command": "ls"}, "cwd": str(repository_root)}
357
- )
358
- result = run_hook(payload, repository_root)
359
- assert is_allow(result)
360
-
361
-
362
- def test_clean_surface_allows(tmp_path: Path) -> None:
363
- repository_root = tmp_path / "repo"
364
- repository_root.mkdir()
365
- initialize_repository(repository_root)
366
- run_git(repository_root, "checkout", "-b", "feature")
367
- write_working_tree_file(repository_root, "feature.py", CLEAN_MODULE_SOURCE)
368
- payload = write_agent_payload("code-verifier", "verify the change", repository_root)
369
- result = run_hook(payload, repository_root)
370
- assert is_allow(result)
371
-
372
-
373
- def test_real_conflict_denies_naming_files(tmp_path: Path) -> None:
374
- repository_root = make_conflict_repository(tmp_path)
375
- payload = write_agent_payload("code-verifier", "verify the change", repository_root)
376
- result = run_hook(payload, repository_root)
377
- assert not is_allow(result)
378
- reason = deny_reason(result)
379
- assert "shared.py" in reason
380
- assert "Merge conflicts vs" in reason
381
-
382
-
383
- def test_real_code_rules_violation_on_added_line_denies(tmp_path: Path) -> None:
384
- repository_root = tmp_path / "repo"
385
- repository_root.mkdir()
386
- initialize_repository(repository_root)
387
- run_git(repository_root, "checkout", "-b", "feature")
388
- commit_file(repository_root, "tracked.py", CLEAN_MODULE_SOURCE, "add tracked")
389
- write_working_tree_file(repository_root, "tracked.py", VIOLATING_MODULE_SOURCE)
390
- payload = write_agent_payload("code-verifier", "verify the change", repository_root)
391
- result = run_hook(payload, repository_root)
392
- assert not is_allow(result)
393
- reason = deny_reason(result)
394
- assert "tracked.py" in reason
395
- assert "Line " in reason
396
-
397
-
398
- def test_committed_on_branch_violation_with_clean_working_tree_denies(tmp_path: Path) -> None:
399
- repository_root = tmp_path / "repo"
400
- repository_root.mkdir()
401
- initialize_repository(repository_root)
402
- run_git(repository_root, "checkout", "-b", "feature")
403
- commit_file(repository_root, "committed.py", VIOLATING_MODULE_SOURCE, "commit violation")
404
- status_output = run_git(repository_root, "status", "--porcelain")
405
- assert status_output == "", "working tree must be clean so the deny comes from the committed line"
406
- payload = write_agent_payload("code-verifier", "verify the change", repository_root)
407
- result = run_hook(payload, repository_root)
408
- assert not is_allow(result)
409
- reason = deny_reason(result)
410
- assert "committed.py" in reason
411
- assert "Line " in reason
412
-
413
-
414
- def test_preexisting_violation_on_untouched_line_allows(tmp_path: Path) -> None:
415
- repository_root = tmp_path / "repo"
416
- repository_root.mkdir()
417
- initialize_repository(repository_root)
418
- carrier_sha = commit_file(
419
- repository_root, "carrier.py", PREEXISTING_VIOLATION_BASE_SOURCE, "add carrier"
420
- )
421
- run_git(repository_root, "update-ref", "refs/remotes/origin/main", carrier_sha)
422
- run_git(repository_root, "checkout", "-b", "feature")
423
- write_working_tree_file(repository_root, "carrier.py", PREEXISTING_VIOLATION_EDITED_SOURCE)
424
- payload = write_agent_payload("code-verifier", "verify the change", repository_root)
425
- result = run_hook(payload, repository_root)
426
- assert is_allow(result)
427
-
428
-
429
- def test_untracked_new_violating_file_denies(tmp_path: Path) -> None:
430
- repository_root = tmp_path / "repo"
431
- repository_root.mkdir()
432
- initialize_repository(repository_root)
433
- run_git(repository_root, "checkout", "-b", "feature")
434
- write_working_tree_file(repository_root, "fresh.py", VIOLATING_MODULE_SOURCE)
435
- payload = write_agent_payload("code-verifier", "verify the change", repository_root)
436
- result = run_hook(payload, repository_root)
437
- assert not is_allow(result)
438
- reason = deny_reason(result)
439
- assert "fresh.py" in reason
440
-
441
-
442
- def test_tooling_scratch_file_is_ignored(tmp_path: Path) -> None:
443
- repository_root = tmp_path / "repo"
444
- repository_root.mkdir()
445
- initialize_repository(repository_root)
446
- run_git(repository_root, "checkout", "-b", "feature")
447
- scratch_directory = repository_root / ".claude" / "verification"
448
- scratch_directory.mkdir(parents=True)
449
- (scratch_directory / "x.py").write_text(VIOLATING_MODULE_SOURCE, encoding="utf-8")
450
- payload = write_agent_payload("code-verifier", "verify the change", repository_root)
451
- result = run_hook(payload, repository_root)
452
- assert is_allow(result)
453
-
454
-
455
- def test_missing_base_ref_fails_open(tmp_path: Path) -> None:
456
- repository_root = tmp_path / "repo"
457
- repository_root.mkdir()
458
- run_git(repository_root, "init")
459
- run_git(repository_root, "config", "user.email", "tests@example.com")
460
- run_git(repository_root, "config", "user.name", "Preflight Tests")
461
- disable_native_git_hooks(repository_root)
462
- (repository_root / "base.py").write_text(CLEAN_MODULE_SOURCE, encoding="utf-8")
463
- run_git(repository_root, "add", "base.py")
464
- run_git(repository_root, "commit", "-m", "initial")
465
- payload = write_agent_payload("code-verifier", "verify the change", repository_root)
466
- result = run_hook(payload, repository_root)
467
- assert is_allow(result)
468
-
469
-
470
- def test_non_repo_cwd_fails_open(tmp_path: Path) -> None:
471
- non_repo_directory = tmp_path / "plain"
472
- non_repo_directory.mkdir()
473
- payload = write_agent_payload("code-verifier", "verify the change", non_repo_directory)
474
- result = run_hook(payload, non_repo_directory)
475
- assert is_allow(result)
476
-
477
-
478
- def test_behind_but_conflict_free_allows(tmp_path: Path) -> None:
479
- repository_root = tmp_path / "repo"
480
- repository_root.mkdir()
481
- initialize_repository(repository_root)
482
- base_sha = commit_file(repository_root, "shared.py", SHARED_BASE_SOURCE, "add shared")
483
- run_git(repository_root, "checkout", "-b", "feature")
484
- commit_file(repository_root, "shared.py", SHARED_FEATURE_SOURCE, "feature edit")
485
- advance_origin_main_divergent(repository_root, base_sha, "other.py", OTHER_DIVERGENT_SOURCE)
486
- payload = write_agent_payload("code-verifier", "verify the change", repository_root)
487
- result = run_hook(payload, repository_root)
488
- assert is_allow(result)
489
-
490
-
491
- def test_unreadable_changed_file_alone_fails_open(tmp_path: Path) -> None:
492
- repository_root = tmp_path / "repo"
493
- repository_root.mkdir()
494
- initialize_repository(repository_root)
495
- run_git(repository_root, "checkout", "-b", "feature")
496
- write_invalid_utf8_file(repository_root, "binary.py")
497
- payload = write_agent_payload("code-verifier", "verify the change", repository_root)
498
- result = run_hook(payload, repository_root)
499
- assert is_allow(result)
500
-
501
-
502
- def test_unreadable_changed_file_does_not_mask_real_violation(tmp_path: Path) -> None:
503
- repository_root = tmp_path / "repo"
504
- repository_root.mkdir()
505
- initialize_repository(repository_root)
506
- run_git(repository_root, "checkout", "-b", "feature")
507
- write_invalid_utf8_file(repository_root, "binary.py")
508
- write_working_tree_file(repository_root, "fresh.py", VIOLATING_MODULE_SOURCE)
509
- payload = write_agent_payload("code-verifier", "verify the change", repository_root)
510
- result = run_hook(payload, repository_root)
511
- assert not is_allow(result)
512
- reason = deny_reason(result)
513
- assert "fresh.py" in reason
514
-
515
-
516
- def test_conflict_and_violation_single_deny_names_both(tmp_path: Path) -> None:
517
- repository_root = make_conflict_repository(tmp_path)
518
- write_working_tree_file(repository_root, "violator.py", VIOLATING_MODULE_SOURCE)
519
- payload = write_agent_payload("code-verifier", "verify the change", repository_root)
520
- result = run_hook(payload, repository_root)
521
- assert not is_allow(result)
522
- reason = deny_reason(result)
523
- assert "Merge conflicts vs" in reason
524
- assert "shared.py" in reason
525
- assert "CODE_RULES violations on changed lines:" in reason
526
- assert "violator.py" in reason
527
-
528
-
529
- def test_stale_self_upstream_still_judges_default_branch(tmp_path: Path) -> None:
530
- repository_root = make_stale_self_upstream_conflict_repository(tmp_path)
531
- tracked_upstream = run_git(
532
- repository_root, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"
533
- )
534
- assert tracked_upstream == "origin/feature", "fixture must track the branch's own remote ref"
535
- payload = write_agent_payload("code-verifier", "verify the change", repository_root)
536
- result = run_hook(payload, repository_root)
537
- assert not is_allow(result)
538
- reason = deny_reason(result)
539
- assert "shared.py" in reason
540
- assert "Merge conflicts vs" in reason
541
-
542
-
543
- def test_staged_resolved_merge_allows(tmp_path: Path) -> None:
544
- repository_root = make_merge_in_progress_repository(tmp_path)
545
- start_conflicting_merge(repository_root)
546
- write_working_tree_file(repository_root, "shared.py", SHARED_FEATURE_SOURCE)
547
- run_git(repository_root, "add", "shared.py")
548
- remaining_unmerged = run_git(repository_root, "diff", "--name-only", "--diff-filter=U")
549
- assert remaining_unmerged == "", "the staged resolution must clear every unmerged index entry"
550
- merge_head_sha = run_git(repository_root, "rev-parse", "--verify", "--quiet", "MERGE_HEAD")
551
- assert merge_head_sha, "a merge must still be in progress so MERGE_HEAD is present"
552
- payload = write_agent_payload("code-verifier", "verify the change", repository_root)
553
- result = run_hook(payload, repository_root)
554
- assert is_allow(result)
555
-
556
-
557
- def test_unresolved_merge_keeps_denying(tmp_path: Path) -> None:
558
- repository_root = make_merge_in_progress_repository(tmp_path)
559
- start_conflicting_merge(repository_root)
560
- remaining_unmerged = run_git(repository_root, "diff", "--name-only", "--diff-filter=U")
561
- assert "shared.py" in remaining_unmerged, "the merge must leave shared.py unmerged"
562
- payload = write_agent_payload("code-verifier", "verify the change", repository_root)
563
- result = run_hook(payload, repository_root)
564
- assert not is_allow(result)
565
- reason = deny_reason(result)
566
- assert "shared.py" in reason
567
-
568
-
569
- def test_hook_imports_real_config_when_parent_holds_shadowing_config(
570
- tmp_path: Path,
571
- ) -> None:
572
- real_hooks_directory = HOOK_PATH.parent.parent
573
- real_package_directory = real_hooks_directory.parent
574
-
575
- staged_package_directory = tmp_path / "claude-dev-env"
576
- shutil.copytree(
577
- real_hooks_directory,
578
- staged_package_directory / "hooks",
579
- )
580
- shutil.copytree(
581
- real_package_directory / "_shared",
582
- staged_package_directory / "_shared",
583
- )
584
-
585
- shadowing_config_directory = staged_package_directory / "hooks" / "config"
586
- shadowing_config_directory.mkdir(parents=True, exist_ok=True)
587
- (shadowing_config_directory / "__init__.py").write_text("", encoding="utf-8")
588
- (shadowing_config_directory / "unrelated_constants.py").write_text(
589
- "UNRELATED_VALUE = 1\n", encoding="utf-8"
590
- )
591
-
592
- staged_hook = (
593
- staged_package_directory
594
- / "hooks"
595
- / "blocking"
596
- / "code_verifier_spawn_preflight_gate.py"
597
- )
598
- payload = write_agent_payload("general-purpose", "unrelated work", tmp_path)
599
- completed = subprocess.run(
600
- [sys.executable, str(staged_hook)],
601
- check=False,
602
- input=payload,
603
- capture_output=True,
604
- text=True,
605
- cwd=str(tmp_path),
606
- timeout=120,
607
- )
608
-
609
- assert "ModuleNotFoundError" not in completed.stderr
610
- assert completed.returncode == 0
611
-
612
-
613
- def test_task_tool_code_verifier_spawn_is_gated_like_agent(tmp_path: Path) -> None:
614
- repository_root = tmp_path / "repo"
615
- repository_root.mkdir()
616
- initialize_repository(repository_root)
617
- run_git(repository_root, "checkout", "-b", "feature")
618
- write_working_tree_file(repository_root, "fresh.py", VIOLATING_MODULE_SOURCE)
619
- task_payload = write_task_payload("code-verifier", "verify the change", repository_root)
620
- task_result = run_hook(task_payload, repository_root)
621
- assert not is_allow(task_result)
622
- task_reason = deny_reason(task_result)
623
- assert "fresh.py" in task_reason
624
- assert "CODE_RULES violations on changed lines:" in task_reason
625
- clean_coder_payload = write_task_payload("clean-coder", "write code", repository_root)
626
- clean_coder_result = run_hook(clean_coder_payload, repository_root)
627
- assert is_allow(clean_coder_result)
628
-
629
-
630
- def stage_package_and_feature_repo(tmp_path: Path) -> tuple[Path, Path]:
631
- """Copy the hook package and build a clean feature repo for load-failure tests.
632
-
633
- Args:
634
- tmp_path: The pytest temporary directory the staged copy lands under.
635
-
636
- Returns:
637
- A ``(staged_enforcer_path, repository_root)`` pair: the copied
638
- ``code_rules_enforcer.py`` a test renders unloadable, and the clean
639
- feature repository the staged hook runs against.
640
- """
641
- real_hooks_directory = HOOK_PATH.parent.parent
642
- real_package_directory = real_hooks_directory.parent
643
- staged_package_directory = tmp_path / "claude-dev-env"
644
- shutil.copytree(real_hooks_directory, staged_package_directory / "hooks")
645
- shutil.copytree(
646
- real_package_directory / "_shared", staged_package_directory / "_shared"
647
- )
648
- enforcer_path = staged_package_directory / "hooks" / "blocking" / "code_rules_enforcer.py"
649
- repository_root = tmp_path / "repo"
650
- repository_root.mkdir()
651
- initialize_repository(repository_root)
652
- run_git(repository_root, "checkout", "-b", "feature")
653
- write_working_tree_file(repository_root, "feature.py", CLEAN_MODULE_SOURCE)
654
- return enforcer_path, repository_root
655
-
656
-
657
- def run_staged_preflight_hook(
658
- enforcer_path: Path, repository_root: Path
659
- ) -> subprocess.CompletedProcess[str]:
660
- """Run the staged preflight hook that sits beside *enforcer_path*.
661
-
662
- Args:
663
- enforcer_path: The staged ``code_rules_enforcer.py`` path; the staged
664
- hook is its sibling in the same ``hooks/blocking`` directory.
665
- repository_root: The feature repository the hook inspects.
666
-
667
- Returns:
668
- The completed hook subprocess run.
669
- """
670
- staged_hook = enforcer_path.parent / "code_verifier_spawn_preflight_gate.py"
671
- payload = write_agent_payload("code-verifier", "verify the change", repository_root)
672
- return subprocess.run(
673
- [sys.executable, str(staged_hook)],
674
- check=False,
675
- input=payload,
676
- capture_output=True,
677
- text=True,
678
- cwd=str(repository_root),
679
- timeout=120,
680
- )
681
-
682
-
683
- def test_engine_load_failure_denies_with_named_reason(tmp_path: Path) -> None:
684
- enforcer_path, repository_root = stage_package_and_feature_repo(tmp_path)
685
- enforcer_path.unlink()
686
- completed = run_staged_preflight_hook(enforcer_path, repository_root)
687
- assert not is_allow(completed)
688
- reason = deny_reason(completed)
689
- assert "CODE_RULES engine failed to load" in reason
690
- assert "load_validate_content" in reason
691
-
692
-
693
- def test_engine_import_error_denies_with_named_reason(tmp_path: Path) -> None:
694
- enforcer_path, repository_root = stage_package_and_feature_repo(tmp_path)
695
- enforcer_path.write_text(BROKEN_ENFORCER_SOURCE, encoding="utf-8")
696
- completed = run_staged_preflight_hook(enforcer_path, repository_root)
697
- assert not is_allow(completed)
698
- reason = deny_reason(completed)
699
- assert "CODE_RULES engine failed to load" in reason
700
- assert "load_validate_content" in reason
701
-
702
-
703
- def test_engine_re_error_denies_with_named_reason(tmp_path: Path) -> None:
704
- enforcer_path, repository_root = stage_package_and_feature_repo(tmp_path)
705
- enforcer_path.write_text(RE_ERROR_ENFORCER_SOURCE, encoding="utf-8")
706
- completed = run_staged_preflight_hook(enforcer_path, repository_root)
707
- assert not is_allow(completed)
708
- reason = deny_reason(completed)
709
- assert "CODE_RULES engine failed to load" in reason
710
- assert "load_validate_content" in reason