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,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