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,212 +0,0 @@
1
- """Decide whether a command builds a stamp path with obfuscation and writes.
2
-
3
- A path-obfuscation forge hides ``.claude`` or ``code-review-stamps`` inside hex
4
- digits, a base64 token, a comma-separated character-code sequence, or a run of
5
- ``+``-joined ``chr(<int>)`` calls, then pairs the assembled path with a
6
- non-redirect file write (``open(``, ``write_text``, ``Out-File``,
7
- ``Set-Content``, ``Add-Content``, ``tee``). The literal path matchers miss such
8
- a path because it carries no plain ``code-review-stamps`` substring, so this
9
- module decodes each embedded token and looks for a stamp-path segment either in
10
- the decoded text or inside the write call's argument region. A bare ``>``
11
- redirect to the stamp path is caught by the literal-path matchers, so a command
12
- whose only write is a redirect is left alone here.
13
-
14
- The entry hook registers the constant packages before importing this module, so
15
- the ``from config...`` imports below resolve against the sibling ``config/``
16
- package without a bootstrap call here.
17
- """
18
-
19
- from __future__ import annotations
20
-
21
- import base64
22
- import binascii
23
- import re
24
-
25
- from config.code_review_enforcement_constants import (
26
- ALL_STAMP_PATH_SEGMENT_BODIES,
27
- ALL_STAMP_PATH_SEGMENT_NAMES,
28
- )
29
- from config.verified_commit_constants import (
30
- BASE64_TOKEN_PATTERN,
31
- CHARACTER_CODE_SEQUENCE_PATTERN,
32
- CHR_CALL_CHAIN_PATTERN,
33
- CHR_CALL_CODE_PATTERN,
34
- HEX_DIGITS_PER_BYTE,
35
- HEX_TOKEN_PATTERN,
36
- NON_REDIRECT_FILE_WRITE_PRIMITIVE_PATTERN,
37
- PATH_OBFUSCATION_PRIMITIVE_PATTERN,
38
- WRITE_CALL_REGION_PATTERN,
39
- )
40
-
41
- _ASCII_ENCODING = "ascii"
42
-
43
-
44
- def _decode_hex_token(hex_token: str) -> str:
45
- """Decode a hex-digit token to text, returning empty text on failure.
46
-
47
- Args:
48
- hex_token: A run of hex digits taken from the command.
49
-
50
- Returns:
51
- The decoded ASCII text, or empty text when the token is odd-length,
52
- not valid hex, or not decodable as ASCII.
53
- """
54
- odd_hex_token_remainder = 1
55
- if len(hex_token) % HEX_DIGITS_PER_BYTE == odd_hex_token_remainder:
56
- return ""
57
- try:
58
- return bytes.fromhex(hex_token).decode(_ASCII_ENCODING)
59
- except (ValueError, binascii.Error):
60
- return ""
61
-
62
-
63
- def _decode_base64_token(base64_token: str) -> str:
64
- """Decode a base64 token to text, returning empty text on failure.
65
-
66
- Args:
67
- base64_token: A base64-shaped token taken from the command.
68
-
69
- Returns:
70
- The decoded ASCII text, or empty text when the token is not valid
71
- base64 or not decodable as ASCII.
72
- """
73
- try:
74
- return base64.b64decode(base64_token, validate=True).decode(_ASCII_ENCODING)
75
- except (ValueError, binascii.Error):
76
- return ""
77
-
78
-
79
- def _decode_character_code_sequence(code_sequence: str) -> str:
80
- """Decode a comma-separated character-code sequence to text.
81
-
82
- Args:
83
- code_sequence: A comma-separated run of decimal character codes, each
84
- within the three-digit range the source pattern admits.
85
-
86
- Returns:
87
- The decoded text.
88
- """
89
- character_code_separator = ","
90
- decoded_characters: list[str] = []
91
- for each_code in code_sequence.split(character_code_separator):
92
- code_point = int(each_code.strip())
93
- decoded_characters.append(chr(code_point))
94
- return "".join(decoded_characters)
95
-
96
-
97
- def _decode_chr_call_chain(chr_chain: str) -> str:
98
- """Decode a run of ``+``-joined ``chr(<int>)`` calls to text.
99
-
100
- Args:
101
- chr_chain: A run of two or more ``+``-joined ``chr(<int>)`` calls taken
102
- from the command, each code within the three-digit source range.
103
-
104
- Returns:
105
- The decoded text.
106
- """
107
- decoded_characters: list[str] = []
108
- for each_code in re.findall(CHR_CALL_CODE_PATTERN, chr_chain, re.IGNORECASE):
109
- decoded_characters.append(chr(int(each_code)))
110
- return "".join(decoded_characters)
111
-
112
-
113
- def _append_when_present(all_decoded_texts: list[str], decoded_text: str) -> None:
114
- """Append the lowercase decoded text when it is non-empty.
115
-
116
- Args:
117
- all_decoded_texts: The running list of decoded texts to extend.
118
- decoded_text: One token's decoded text, empty when the token failed.
119
-
120
- Returns:
121
- None. The list is extended in place when the text is non-empty.
122
- """
123
- if decoded_text:
124
- all_decoded_texts.append(decoded_text.lower())
125
-
126
-
127
- def _decoded_texts_from_command(command_text: str) -> list[str]:
128
- """Decode every hex, base64, character-code, and chr-chain token a command embeds.
129
-
130
- Args:
131
- command_text: The raw command string from the tool payload.
132
-
133
- Returns:
134
- The lowercase decoded text for every token that decodes cleanly;
135
- tokens that fail to decode or carry an out-of-range code are skipped.
136
- """
137
- all_decoded_texts: list[str] = []
138
- for each_hex_token in re.findall(HEX_TOKEN_PATTERN, command_text, re.IGNORECASE):
139
- _append_when_present(all_decoded_texts, _decode_hex_token(each_hex_token))
140
- for each_base64_token in re.findall(BASE64_TOKEN_PATTERN, command_text):
141
- _append_when_present(all_decoded_texts, _decode_base64_token(each_base64_token))
142
- for each_code_sequence in re.findall(CHARACTER_CODE_SEQUENCE_PATTERN, command_text):
143
- _append_when_present(all_decoded_texts, _decode_character_code_sequence(each_code_sequence))
144
- for each_chr_chain in re.findall(CHR_CALL_CHAIN_PATTERN, command_text, re.IGNORECASE):
145
- _append_when_present(all_decoded_texts, _decode_chr_call_chain(each_chr_chain))
146
- return all_decoded_texts
147
-
148
-
149
- def _decoded_token_references_stamp_segment(command_text: str) -> bool:
150
- """Decide whether a decodable token in a command hides a stamp-path segment.
151
-
152
- Args:
153
- command_text: The raw command string from the tool payload.
154
-
155
- Returns:
156
- True when a decodable token decodes to a stamp-path segment body
157
- (``claude`` or ``code-review-stamps``).
158
- """
159
- all_decoded_texts = _decoded_texts_from_command(command_text)
160
- return any(
161
- each_segment_body in each_decoded_text
162
- for each_decoded_text in all_decoded_texts
163
- for each_segment_body in ALL_STAMP_PATH_SEGMENT_BODIES
164
- )
165
-
166
-
167
- def _write_call_region_names_stamp_segment(command_text: str) -> bool:
168
- """Decide whether a write call's argument region names a stamp-path segment.
169
-
170
- Args:
171
- command_text: The raw command string from the tool payload.
172
-
173
- Returns:
174
- True when a stamp-path segment name appears inside a non-redirect write
175
- call's argument region.
176
- """
177
- write_call_region_pattern = re.compile(WRITE_CALL_REGION_PATTERN, re.IGNORECASE)
178
- for each_region_match in write_call_region_pattern.finditer(command_text):
179
- lowercased_region = each_region_match.group(0).lower()
180
- if any(each_name in lowercased_region for each_name in ALL_STAMP_PATH_SEGMENT_NAMES):
181
- return True
182
- return False
183
-
184
-
185
- def references_obfuscated_stamp_path(command_text: str) -> bool:
186
- """Decide whether a command pairs path obfuscation with a stamp-path write.
187
-
188
- ::
189
-
190
- python -c "open(bytes.fromhex('...code-review-stamps...'),'w')" -> True
191
- open(bytes.fromhex('2f746d702f78'),'w') -> False
192
-
193
- Args:
194
- command_text: The raw command string from the tool payload.
195
-
196
- Returns:
197
- True when the command holds a path-obfuscation primitive, a
198
- non-redirect file write, and a stamp-path segment reachable by that
199
- write — either a decoded token anywhere in the command or a plain-text
200
- segment inside the write call's argument region.
201
- """
202
- has_obfuscation_primitive = (
203
- re.search(PATH_OBFUSCATION_PRIMITIVE_PATTERN, command_text) is not None
204
- )
205
- has_non_redirect_write = (
206
- re.search(NON_REDIRECT_FILE_WRITE_PRIMITIVE_PATTERN, command_text) is not None
207
- )
208
- if not (has_obfuscation_primitive and has_non_redirect_write):
209
- return False
210
- if _decoded_token_references_stamp_segment(command_text):
211
- return True
212
- return _write_call_region_names_stamp_segment(command_text)
@@ -1,138 +0,0 @@
1
- """Decide whether a split directory change reaches the stamp directory then runs a command.
2
-
3
- A change into the Claude home (``cd ~/.claude``) followed by a change into a
4
- relative ``code-review-stamps`` directory (``cd code-review-stamps``) lands in
5
- the stamp directory without ever naming the two segments adjacently, so the
6
- absolute-path matcher and the single-step ``cd ~/.claude/code-review-stamps``
7
- matcher both miss it. The trust contract denies every shell command that
8
- reaches the stamp directory, so any command after the second change — a
9
- redirect, ``cp``, ``mv``, ``tee``, ``install``, or a ``python -c`` write — is a
10
- forge vector. An unrelated second change (``cd hooks``) lands elsewhere and
11
- passes.
12
-
13
- The entry hook registers the constant packages before importing this module, so
14
- the ``from config...`` imports below resolve against the sibling ``config/``
15
- package without a bootstrap call here.
16
- """
17
-
18
- from __future__ import annotations
19
-
20
- import re
21
-
22
- from config.code_review_enforcement_constants import STAMP_DIRECTORY_CHANGE_TARGET_PATTERN
23
- from config.verified_commit_constants import (
24
- CLAUDE_HOME_DIRECTORY_NAME,
25
- CLAUDE_HOME_TARGET_BOUNDARY_PATTERN,
26
- COMMAND_AFTER_DIRECTORY_CHANGE_PATTERN,
27
- DIRECTORY_CHANGE_OPTION_PREFIX_PATTERN,
28
- DIRECTORY_CHANGE_OPTION_TERMINATOR,
29
- DIRECTORY_CHANGE_PATH_OPTIONS,
30
- DIRECTORY_CHANGE_PATTERN_PREFIX,
31
- DIRECTORY_CHANGE_PATTERN_SUFFIX,
32
- DIRECTORY_CHANGE_TARGET_PATTERN,
33
- DIRECTORY_CHANGE_VERBS,
34
- )
35
- from config.verified_commit_gate_output_constants import REGEX_ALTERNATION_SEPARATOR
36
-
37
-
38
- def _directory_change_verbs_pattern() -> str:
39
- """Build the alternation of directory-change verbs for a change matcher.
40
-
41
- Returns:
42
- The regex alternation of escaped directory-change verbs in sorted order.
43
- """
44
- return REGEX_ALTERNATION_SEPARATOR.join(
45
- re.escape(each_verb) for each_verb in sorted(DIRECTORY_CHANGE_VERBS)
46
- )
47
-
48
-
49
- def _directory_change_option_prefix_pattern() -> str:
50
- """Build the optional path-option prefix that may precede a change target.
51
-
52
- Returns:
53
- The regex matching zero or more leading path-option or terminator
54
- tokens, ready to sit between the change suffix and target patterns.
55
- """
56
- option_alternation = REGEX_ALTERNATION_SEPARATOR.join(
57
- re.escape(each_option)
58
- for each_option in sorted(
59
- DIRECTORY_CHANGE_PATH_OPTIONS | {DIRECTORY_CHANGE_OPTION_TERMINATOR}
60
- )
61
- )
62
- return DIRECTORY_CHANGE_OPTION_PREFIX_PATTERN % option_alternation
63
-
64
-
65
- def directory_change_prefix() -> str:
66
- """Build the change-verb prefix shared by the directory-change matchers.
67
-
68
- The stamp-directory write-blocker reuses this same prefix, so a single
69
- builder feeds both this module's split-change matchers and the blocker's
70
- single-step change matchers, and the two never drift.
71
-
72
- Returns:
73
- The regex prefix matching a directory-change verb and any leading
74
- path-option tokens, ready for a target pattern to follow.
75
- """
76
- return (
77
- DIRECTORY_CHANGE_PATTERN_PREFIX
78
- + _directory_change_verbs_pattern()
79
- + DIRECTORY_CHANGE_PATTERN_SUFFIX
80
- + _directory_change_option_prefix_pattern()
81
- )
82
-
83
-
84
- def _change_into_claude_pattern() -> re.Pattern[str]:
85
- """Build the compiled matcher for a directory change into the Claude home.
86
-
87
- Returns:
88
- The compiled, case-insensitive pattern matching a change verb into any
89
- path ending in ``.claude``.
90
- """
91
- return re.compile(
92
- directory_change_prefix()
93
- + DIRECTORY_CHANGE_TARGET_PATTERN
94
- + re.escape(CLAUDE_HOME_DIRECTORY_NAME)
95
- + CLAUDE_HOME_TARGET_BOUNDARY_PATTERN,
96
- re.IGNORECASE,
97
- )
98
-
99
-
100
- def _change_into_stamp_pattern() -> re.Pattern[str]:
101
- """Build the compiled matcher for a directory change into the stamp directory.
102
-
103
- Returns:
104
- The compiled, case-insensitive pattern matching a change verb into a
105
- relative ``code-review-stamps`` directory.
106
- """
107
- return re.compile(
108
- directory_change_prefix() + STAMP_DIRECTORY_CHANGE_TARGET_PATTERN, re.IGNORECASE
109
- )
110
-
111
-
112
- def changes_through_split_directory_into_stamp(command_text: str) -> bool:
113
- """Decide whether a split change reaches the stamp directory then runs a command.
114
-
115
- ::
116
-
117
- cd ~/.claude && cd code-review-stamps && echo x > f.json -> True
118
- cd ~/.claude && cd hooks && echo x > f.json -> False
119
-
120
- Args:
121
- command_text: The raw command string from the tool payload.
122
-
123
- Returns:
124
- True when a change into the Claude home precedes a change into a
125
- relative stamp directory that is itself followed by any further command.
126
- """
127
- change_into_claude_match = _change_into_claude_pattern().search(command_text)
128
- if change_into_claude_match is None:
129
- return False
130
- change_into_stamp_match = _change_into_stamp_pattern().search(
131
- command_text, change_into_claude_match.end()
132
- )
133
- if change_into_stamp_match is None:
134
- return False
135
- command_after_change_pattern = re.compile(COMMAND_AFTER_DIRECTORY_CHANGE_PATTERN)
136
- return (
137
- command_after_change_pattern.search(command_text, change_into_stamp_match.end()) is not None
138
- )
@@ -1,49 +0,0 @@
1
- """Behavioral tests for the obfuscated-stamp-path matcher.
2
-
3
- The matcher blocks a command that assembles a stamp path from hex, base64, or
4
- ``chr(<int>)`` codes and pairs it with a non-redirect file write, while a
5
- decode-to-other-path one-liner passes.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- from code_review_stamp_write_blocker_parts.obfuscated_stamp_path_reference import (
11
- references_obfuscated_stamp_path,
12
- )
13
-
14
- STAMP_PATH_TEXT = "/.claude/code-review-stamps/a.json"
15
- BENIGN_PATH_TEXT = "/tmp/x"
16
-
17
-
18
- def _hex_forge_command(path_text: str) -> str:
19
- return f"python -c \"open(bytes.fromhex('{path_text.encode().hex()}'),'w').write('x')\""
20
-
21
-
22
- def _chr_chain_forge_command(path_text: str) -> str:
23
- chr_chain = "+".join(f"chr({ord(each_character)})" for each_character in path_text)
24
- return f"python -c \"open({chr_chain},'w').write('x')\""
25
-
26
-
27
- def _character_code_forge_command(path_text: str) -> str:
28
- comma_separated_codes = ",".join(str(ord(each_character)) for each_character in path_text)
29
- return f"python -c \"open(bytes([{comma_separated_codes}]),'w').write('x')\""
30
-
31
-
32
- def test_hex_assembled_stamp_path_write_is_blocked() -> None:
33
- assert references_obfuscated_stamp_path(_hex_forge_command(STAMP_PATH_TEXT))
34
-
35
-
36
- def test_character_code_assembled_stamp_path_write_is_blocked() -> None:
37
- assert references_obfuscated_stamp_path(_character_code_forge_command(STAMP_PATH_TEXT))
38
-
39
-
40
- def test_chr_chain_assembled_stamp_path_write_is_blocked() -> None:
41
- assert references_obfuscated_stamp_path(_chr_chain_forge_command(STAMP_PATH_TEXT))
42
-
43
-
44
- def test_decode_to_other_path_write_is_not_blocked() -> None:
45
- assert not references_obfuscated_stamp_path(_hex_forge_command(BENIGN_PATH_TEXT))
46
-
47
-
48
- def test_redirect_only_stamp_reference_is_left_to_literal_matchers() -> None:
49
- assert not references_obfuscated_stamp_path("echo x > ~/.claude/code-review-stamps/a.json")
@@ -1,38 +0,0 @@
1
- """Behavioral tests for the split directory-change-into-stamp matcher.
2
-
3
- The matcher blocks a command that changes into the Claude home, then into a
4
- relative ``code-review-stamps`` directory, then runs any further command, while
5
- an unrelated second change or a benign command passes.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- from code_review_stamp_write_blocker_parts.split_directory_change_into_stamp import (
11
- changes_through_split_directory_into_stamp,
12
- )
13
-
14
- SPLIT_CHANGE_FORGE = "cd ~/.claude && cd code-review-stamps && echo x > f.json"
15
- UNRELATED_SECOND_CHANGE = "cd ~/.claude && cd hooks && echo x > f.json"
16
- PYTEST_RUN = "python -m pytest test_code_review_stamp_store.py"
17
-
18
-
19
- def test_split_change_into_stamp_then_command_is_blocked() -> None:
20
- assert changes_through_split_directory_into_stamp(SPLIT_CHANGE_FORGE)
21
-
22
-
23
- def test_unrelated_second_change_is_not_blocked() -> None:
24
- assert not changes_through_split_directory_into_stamp(UNRELATED_SECOND_CHANGE)
25
-
26
-
27
- def test_pytest_run_is_not_blocked() -> None:
28
- assert not changes_through_split_directory_into_stamp(PYTEST_RUN)
29
-
30
-
31
- def test_change_into_stamp_without_following_command_is_not_blocked() -> None:
32
- assert not changes_through_split_directory_into_stamp("cd ~/.claude && cd code-review-stamps")
33
-
34
-
35
- def test_pushd_split_change_into_stamp_then_command_is_blocked() -> None:
36
- assert changes_through_split_directory_into_stamp(
37
- "pushd ~/.claude && pushd code-review-stamps && cp a b"
38
- )