claude-dev-env 1.58.0 → 1.60.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 (106) hide show
  1. package/CLAUDE.md +2 -2
  2. package/_shared/pr-loop/scripts/code_rules_gate.py +36 -3
  3. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +6 -0
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
  5. package/_shared/pr-loop/scripts/reviews_disabled.py +12 -0
  6. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +265 -0
  7. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +29 -0
  8. package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +1 -1
  9. package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -0
  10. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  11. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
  12. package/bin/install.mjs +100 -27
  13. package/bin/install.test.mjs +133 -1
  14. package/docs/CODE_RULES.md +3 -3
  15. package/hooks/blocking/code_rules_annotations_length.py +153 -0
  16. package/hooks/blocking/code_rules_dead_dataclass_field.py +319 -0
  17. package/hooks/blocking/code_rules_dead_module_constant.py +321 -0
  18. package/hooks/blocking/code_rules_duplicate_body.py +439 -0
  19. package/hooks/blocking/code_rules_enforcer.py +190 -21
  20. package/hooks/blocking/code_rules_magic_values.py +98 -0
  21. package/hooks/blocking/code_rules_shared.py +41 -0
  22. package/hooks/blocking/code_rules_typeddict_stub.py +172 -0
  23. package/hooks/blocking/config/__init__.py +5 -0
  24. package/hooks/blocking/config/verified_commit_constants.py +106 -0
  25. package/hooks/blocking/destructive_command_blocker.py +1027 -12
  26. package/hooks/blocking/hook_prose_detector_consistency.py +150 -0
  27. package/hooks/blocking/subprocess_budget_completeness.py +380 -0
  28. package/hooks/blocking/test_code_rules_enforcer_annotations.py +225 -0
  29. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
  30. package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +146 -0
  31. package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +467 -0
  32. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +188 -0
  33. package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +330 -0
  34. package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +179 -0
  35. package/hooks/blocking/test_code_rules_enforcer_magic_slice_bounds.py +133 -0
  36. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +415 -0
  37. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +156 -0
  38. package/hooks/blocking/test_destructive_command_blocker.py +622 -3
  39. package/hooks/blocking/test_hook_prose_detector_consistency.py +265 -0
  40. package/hooks/blocking/test_subprocess_budget_completeness.py +588 -0
  41. package/hooks/blocking/test_verdict_directory_write_blocker.py +720 -0
  42. package/hooks/blocking/test_verification_verdict_store.py +278 -0
  43. package/hooks/blocking/test_verified_commit_gate.py +368 -0
  44. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -0
  45. package/hooks/blocking/test_verifier_verdict_minter.py +214 -0
  46. package/hooks/blocking/test_workflow_substitution_slot_blocker.py +242 -0
  47. package/hooks/blocking/verdict_directory_write_blocker.py +667 -0
  48. package/hooks/blocking/verification_verdict_store.py +446 -0
  49. package/hooks/blocking/verified_commit_gate.py +523 -0
  50. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +152 -0
  51. package/hooks/blocking/verifier_verdict_minter.py +299 -0
  52. package/hooks/blocking/workflow_substitution_slot_blocker.py +159 -0
  53. package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
  54. package/hooks/hooks.json +58 -1
  55. package/hooks/hooks_constants/blocking_check_limits.py +1 -0
  56. package/hooks/hooks_constants/code_rules_enforcer_constants.py +16 -0
  57. package/hooks/hooks_constants/dead_dataclass_field_constants.py +25 -0
  58. package/hooks/hooks_constants/dead_module_constant_constants.py +20 -0
  59. package/hooks/hooks_constants/destructive_command_segment_constants.py +178 -0
  60. package/hooks/hooks_constants/duplicate_function_body_constants.py +34 -0
  61. package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +30 -0
  62. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
  63. package/hooks/hooks_constants/subprocess_budget_completeness_constants.py +5 -0
  64. package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +22 -0
  65. package/package.json +1 -1
  66. package/rules/docstring-prose-matches-implementation.md +43 -0
  67. package/rules/file-global-constants.md +7 -1
  68. package/rules/hook-prose-matches-detector.md +26 -0
  69. package/rules/no-cross-skill-duplicate-helpers.md +29 -0
  70. package/rules/no-inline-destructive-literals.md +11 -0
  71. package/rules/workflow-substitution-slots.md +7 -0
  72. package/skills/_shared/pr-loop/scripts/preflight_worktree.py +392 -0
  73. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +70 -0
  74. package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +263 -0
  75. package/skills/autoconverge/SKILL.md +67 -19
  76. package/skills/autoconverge/reference/closing-report.md +59 -17
  77. package/skills/autoconverge/reference/convergence.md +7 -3
  78. package/skills/autoconverge/reference/stop-conditions.md +7 -2
  79. package/skills/autoconverge/workflow/aggregate_runs.py +371 -0
  80. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +193 -76
  81. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +76 -0
  82. package/skills/autoconverge/workflow/converge.contract.test.mjs +206 -206
  83. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +265 -0
  84. package/skills/autoconverge/workflow/converge.mjs +234 -42
  85. package/skills/autoconverge/workflow/convergence_summary.py +110 -0
  86. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +2 -0
  87. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +7 -0
  88. package/skills/autoconverge/workflow/render_report.py +488 -397
  89. package/skills/autoconverge/workflow/test_aggregate_runs.py +134 -0
  90. package/skills/autoconverge/workflow/test_convergence_summary.py +132 -0
  91. package/skills/autoconverge/workflow/test_render_report.py +488 -259
  92. package/skills/pr-converge/reference/per-tick.md +28 -8
  93. package/skills/pr-converge/scripts/check_convergence.py +195 -64
  94. package/skills/pr-converge/scripts/test_check_convergence.py +173 -2
  95. package/skills/rebase/SKILL.md +2 -4
  96. package/skills/update/SKILL.md +37 -5
  97. package/system-prompts/software-engineer.xml +2 -6
  98. package/hooks/blocking/content_search_to_zoekt_redirector.py +0 -59
  99. package/hooks/blocking/content_search_zoekt_bash_block_reason.py +0 -25
  100. package/hooks/blocking/content_search_zoekt_block_payload.py +0 -21
  101. package/hooks/blocking/content_search_zoekt_indexed_paths.py +0 -24
  102. package/hooks/blocking/content_search_zoekt_indexed_roots_config.py +0 -131
  103. package/hooks/blocking/content_search_zoekt_redirect_guidance.py +0 -52
  104. package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +0 -61
  105. package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +0 -92
  106. package/hooks/blocking/test_content_search_zoekt_indexed_roots_config.py +0 -102
@@ -26,6 +26,7 @@ from hooks_constants.blocking_check_limits import ( # noqa: E402
26
26
  MAX_STUB_IMPLEMENTATION_ISSUES,
27
27
  MAX_THIN_WRAPPER_ISSUES,
28
28
  MAX_TYPED_DICT_PAIR_ISSUES,
29
+ MAX_ZERO_PAYLOAD_ALIAS_ISSUES,
29
30
  )
30
31
 
31
32
  def _pascal_to_snake_case(pascal_name: str) -> str:
@@ -58,6 +59,16 @@ def _collect_module_function_names(parsed_tree: ast.AST) -> set[str]:
58
59
  return module_function_names
59
60
 
60
61
 
62
+ def _collect_module_function_nodes_by_name(
63
+ parsed_tree: ast.AST,
64
+ ) -> dict[str, ast.FunctionDef | ast.AsyncFunctionDef]:
65
+ function_node_by_name: dict[str, ast.FunctionDef | ast.AsyncFunctionDef] = {}
66
+ for each_statement in parsed_tree.body:
67
+ if isinstance(each_statement, (ast.FunctionDef, ast.AsyncFunctionDef)):
68
+ function_node_by_name[each_statement.name] = each_statement
69
+ return function_node_by_name
70
+
71
+
61
72
  def _is_init_file(file_path: str) -> bool:
62
73
  return file_path.replace("\\", "/").rsplit("/", 1)[-1] == "__init__.py"
63
74
 
@@ -126,6 +137,167 @@ def check_thin_wrapper_files(content: str, file_path: str) -> list[str]:
126
137
  return issues[:MAX_THIN_WRAPPER_ISSUES]
127
138
 
128
139
 
140
+ def _function_parameter_names_in_order(
141
+ function_node: ast.FunctionDef | ast.AsyncFunctionDef,
142
+ ) -> list[str]:
143
+ arguments = function_node.args
144
+ positional_arguments = [*arguments.posonlyargs, *arguments.args]
145
+ return [each_argument.arg for each_argument in positional_arguments]
146
+
147
+
148
+ def _has_only_positional_parameters(
149
+ function_node: ast.FunctionDef | ast.AsyncFunctionDef,
150
+ ) -> bool:
151
+ arguments = function_node.args
152
+ has_no_parameter_defaults = not arguments.defaults and not arguments.kw_defaults
153
+ return (
154
+ not arguments.kwonlyargs
155
+ and arguments.vararg is None
156
+ and arguments.kwarg is None
157
+ and has_no_parameter_defaults
158
+ )
159
+
160
+
161
+ def _single_return_call(
162
+ function_node: ast.FunctionDef | ast.AsyncFunctionDef,
163
+ ) -> ast.Call | None:
164
+ body_statements = function_node.body
165
+ statements_after_docstring = (
166
+ body_statements[1:]
167
+ if body_statements and _statement_is_docstring(body_statements[0])
168
+ else body_statements
169
+ )
170
+ if len(statements_after_docstring) != 1:
171
+ return None
172
+ only_statement = statements_after_docstring[0]
173
+ if not isinstance(only_statement, ast.Return):
174
+ return None
175
+ return only_statement.value if isinstance(only_statement.value, ast.Call) else None
176
+
177
+
178
+ def _forwards_parameters_unchanged(call_node: ast.Call, all_parameter_names: list[str]) -> bool:
179
+ if call_node.keywords:
180
+ return False
181
+ if len(call_node.args) != len(all_parameter_names):
182
+ return False
183
+ for each_argument, each_parameter_name in zip(call_node.args, all_parameter_names):
184
+ if not isinstance(each_argument, ast.Name) or each_argument.id != each_parameter_name:
185
+ return False
186
+ return True
187
+
188
+
189
+ def _function_is_async(function_node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
190
+ return isinstance(function_node, ast.AsyncFunctionDef)
191
+
192
+
193
+ def _alias_target_name(call_node: ast.Call, all_sibling_function_names: set[str]) -> str:
194
+ callee = call_node.func
195
+ if not isinstance(callee, ast.Name):
196
+ return ""
197
+ return callee.id if callee.id in all_sibling_function_names else ""
198
+
199
+
200
+ def _module_string_literal_values(parsed_tree: ast.AST) -> set[str]:
201
+ string_literal_values: set[str] = set()
202
+ for each_node in ast.walk(parsed_tree):
203
+ if isinstance(each_node, ast.Constant) and isinstance(each_node.value, str):
204
+ string_literal_values.add(each_node.value)
205
+ return string_literal_values
206
+
207
+
208
+ def _target_accepts_forwarded_positional_call(
209
+ target_node: ast.FunctionDef | ast.AsyncFunctionDef,
210
+ forwarded_argument_count: int,
211
+ ) -> bool:
212
+ arguments = target_node.args
213
+ if any(default is None for default in arguments.kw_defaults):
214
+ return False
215
+ positional_parameters = [*arguments.posonlyargs, *arguments.args]
216
+ total_positional_count = len(positional_parameters)
217
+ required_positional_count = total_positional_count - len(arguments.defaults)
218
+ if arguments.vararg is not None:
219
+ return forwarded_argument_count >= required_positional_count
220
+ return required_positional_count <= forwarded_argument_count <= total_positional_count
221
+
222
+
223
+ def check_zero_payload_function_alias(content: str, file_path: str) -> list[str]:
224
+ """Flag a module-level function that only forwards its parameters to a sibling.
225
+
226
+ A function whose entire body (after an optional docstring) is a single
227
+ `return sibling_function(first_param, second_param, ...)` that forwards its
228
+ own parameters unchanged to another function defined in the same module is a
229
+ second name for one behavior — indirection without payload, which CODE_RULES
230
+ discourages. Callers should invoke the sibling directly. Both `def` and
231
+ `async def` forwarders are inspected.
232
+
233
+ A forwarder is left unflagged when any of these makes a direct call to the
234
+ target not equivalent to the alias: a decorator (caching, `@property`, route
235
+ registration); a parameter carrying a default value the target rejects; a
236
+ keyword-only / `*args` / `**kwargs` parameter on the alias; an awaitability
237
+ mismatch where one of the alias and target is `async def` and the other is
238
+ not; a forwarded positional call the live target's signature rejects (the
239
+ target has a keyword-only parameter without a default, or its positional
240
+ arity does not admit the forwarded argument count); or a name dispatched by a
241
+ string literal elsewhere in the module, where the named handler must exist for
242
+ a registry to resolve it.
243
+
244
+ Hook infrastructure is intentionally NOT exempt — pass-through aliases inside
245
+ hook modules are the motivating case. Test files and config files are exempt
246
+ because re-binding aliases are legitimate scaffolding there.
247
+
248
+ Args:
249
+ content: The source under inspection.
250
+ file_path: Path to the file, used for the test and config exemptions.
251
+
252
+ Returns:
253
+ One issue string per pass-through alias, capped at
254
+ MAX_ZERO_PAYLOAD_ALIAS_ISSUES.
255
+ """
256
+ if is_test_file(file_path) or is_config_file(file_path):
257
+ return []
258
+
259
+ try:
260
+ parsed_tree = ast.parse(content)
261
+ except SyntaxError:
262
+ return []
263
+
264
+ function_node_by_name = _collect_module_function_nodes_by_name(parsed_tree)
265
+ all_sibling_function_names = set(function_node_by_name)
266
+ all_string_literal_values = _module_string_literal_values(parsed_tree)
267
+ issues: list[str] = []
268
+ for each_statement in parsed_tree.body:
269
+ if not isinstance(each_statement, (ast.FunctionDef, ast.AsyncFunctionDef)):
270
+ continue
271
+ if each_statement.decorator_list:
272
+ continue
273
+ if each_statement.name in all_string_literal_values:
274
+ continue
275
+ if not _has_only_positional_parameters(each_statement):
276
+ continue
277
+ call_node = _single_return_call(each_statement)
278
+ if call_node is None:
279
+ continue
280
+ target_name = _alias_target_name(call_node, all_sibling_function_names)
281
+ if not target_name or target_name == each_statement.name:
282
+ continue
283
+ target_node = function_node_by_name[target_name]
284
+ if _function_is_async(each_statement) != _function_is_async(target_node):
285
+ continue
286
+ forwarded_parameter_names = _function_parameter_names_in_order(each_statement)
287
+ if not _forwards_parameters_unchanged(call_node, forwarded_parameter_names):
288
+ continue
289
+ if not _target_accepts_forwarded_positional_call(
290
+ target_node, len(forwarded_parameter_names)
291
+ ):
292
+ continue
293
+ issues.append(
294
+ f"Line {each_statement.lineno}: {file_path}: zero-payload alias — "
295
+ f"{each_statement.name} only forwards its parameters to {target_name}; "
296
+ f"callers should call {target_name} directly (indirection without payload)"
297
+ )
298
+ return issues[:MAX_ZERO_PAYLOAD_ALIAS_ISSUES]
299
+
300
+
129
301
  def check_typed_dict_encode_decode(content: str, file_path: str) -> list[str]:
130
302
  """Flag TypedDict declarations missing companion `_encode_*` / `_decode_*` functions."""
131
303
  if (
@@ -0,0 +1,5 @@
1
+ """Configuration package for the blocking hooks.
2
+
3
+ A regular package (not a namespace package) so it resolves ahead of any
4
+ same-named package later on ``sys.path``.
5
+ """
@@ -0,0 +1,106 @@
1
+ """Constants for the verified-commit gate hook family.
2
+
3
+ Shared by ``verification_verdict_store.py``, ``verified_commit_gate.py``,
4
+ and ``verifier_verdict_minter.py`` so every tunable lives in one place.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ GIT_TIMEOUT_SECONDS = 30
10
+ ROOT_KEY_HEX_LENGTH = 16
11
+ VERDICT_JSON_INDENT = 2
12
+ CLAUDE_HOME_DIRECTORY_NAME = ".claude"
13
+ VERDICT_DIRECTORY_NAME = "verification"
14
+ VERDICT_DIRECTORY_NAME_SEPARATOR_PATTERN = r"['\"\\/,\s]+"
15
+ VERDICT_DIRECTORY_PATH_BOUNDARY_PATTERN = r"(?=['\"]*[\\/,])"
16
+ RELATIVE_VERDICT_DIRECTORY_PATTERN = r"(?:^|(?<=[\s;&|(='\"]))verification[\\/]"
17
+ VERDICT_PATH_GLUE_PATTERN = r"['\"+\\/\s]*[\\/]['\"+\\/\s]*"
18
+ VERDICT_DIRECTORY_CHANGE_TARGET_PATTERN = r"[ \t]+['\"]?verification[\\/]?['\"]?(?=[\s;&|]|$)"
19
+ VERDICT_PATH_JOINED_VARIABLE_PATTERN = r"\$\{?(\w+)\}?[\\/]|[\\/]\$\{?(\w+)\}?"
20
+ VERDICT_PATH_VARIABLE_ASSIGNMENT_PATTERN = r"(?:^|(?<=[\s;&|(]))%s=(\S+)"
21
+ VERDICT_FILE_RELATIVE_REFERENCE_PATTERN = (
22
+ rf"(?:^|(?<=[\s;&|(='\"\\/]))verification[\\/][0-9a-f]{{{ROOT_KEY_HEX_LENGTH}}}\.json"
23
+ )
24
+ PATH_OBFUSCATION_PRIMITIVE_PATTERN = (
25
+ r"chr\s*\(|bytes\.fromhex\s*\(|b64decode\s*\(|codecs\.decode\s*\("
26
+ r"|(?:bytes|bytearray)\s*\(\s*\[|\[char\[?\]?\]"
27
+ )
28
+ ALL_VERDICT_PATH_SEGMENT_NAMES = (".claude", "verification")
29
+ ALL_VERDICT_PATH_SEGMENT_BODIES = ("claude", "verification")
30
+ HEX_TOKEN_PATTERN = r"(?<![0-9a-fx])([0-9a-f]{6,})(?![0-9a-f])"
31
+ BASE64_TOKEN_PATTERN = r"[A-Za-z0-9+/]{8,}={0,2}"
32
+ CHARACTER_CODE_SEQUENCE_PATTERN = r"\d{1,3}(?:\s*,\s*\d{1,3})+"
33
+ CHR_CALL_CHAIN_PATTERN = r"chr\(\s*\d{1,3}\s*\)(?:\s*\+\s*chr\(\s*\d{1,3}\s*\))+"
34
+ CHR_CALL_CODE_PATTERN = r"chr\(\s*(\d{1,3})\s*\)"
35
+ HEX_DIGITS_PER_BYTE = 2
36
+ FILE_WRITE_PRIMITIVE_PATTERN = (
37
+ r"\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
38
+ r"|Out-File|Set-Content|Add-Content|\btee\b|>"
39
+ )
40
+ NON_REDIRECT_FILE_WRITE_PRIMITIVE_PATTERN = (
41
+ r"\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
42
+ r"|Out-File|Set-Content|Add-Content|\btee\b"
43
+ )
44
+ WRITE_CALL_REGION_PATTERN = (
45
+ r"(?:\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
46
+ r"|Out-File|Set-Content|Add-Content|\btee\b)[^;&|\n]*"
47
+ )
48
+ VERDICT_KEY_ALL_PASS = "all_pass"
49
+ VERDICT_KEY_MANIFEST_SHA256 = "manifest_sha256"
50
+ DOCS_ONLY_EXTENSIONS = frozenset(
51
+ {".md", ".txt", ".rst", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico"}
52
+ )
53
+ PYTHON_EXTENSION = ".py"
54
+ TEST_FILE_PREFIX = "test_"
55
+ TEST_FILE_SUFFIX = "_test.py"
56
+ CONFTEST_FILE_NAME = "conftest.py"
57
+ MINIMUM_STATUS_FIELD_COUNT = 2
58
+ ALL_FALLBACK_BASE_REFERENCES = ("origin/main", "origin/master")
59
+ ALL_TOOLING_STATE_PREFIXES = (
60
+ ".claude/verification/",
61
+ ".claude/worktrees/",
62
+ ".claude/daemon/",
63
+ ".claude/teams/",
64
+ ".claude/sessions/",
65
+ ".cursor/worktrees/",
66
+ )
67
+ GATED_GIT_SUBCOMMANDS = frozenset({"commit", "push"})
68
+ ALL_GIT_BINARY_NAMES = frozenset({"git", "git.exe"})
69
+ VALUE_TAKING_GIT_OPTIONS = frozenset({"-C", "-c", "--git-dir", "--work-tree", "--namespace"})
70
+ REPO_DIRECTORY_OPTION = "-C"
71
+ WORK_TREE_OPTION = "--work-tree"
72
+ DIRECTORY_CHANGE_VERBS = frozenset({"cd", "pushd", "set-location", "sl"})
73
+ DIRECTORY_CHANGE_PATH_OPTIONS = frozenset({"-path", "-literalpath"})
74
+ DIRECTORY_CHANGE_OPTION_TERMINATOR = "--"
75
+ DIRECTORY_CHANGE_PATTERN_PREFIX = r"(?:^|(?<=[\s;&|(]))(?:"
76
+ DIRECTORY_CHANGE_PATTERN_SUFFIX = r")(?=\s|$)"
77
+ DIRECTORY_CHANGE_OPTION_PREFIX_PATTERN = r"(?:[ \t]+(?:%s)(?=\s|$))*"
78
+ DIRECTORY_CHANGE_TARGET_PATTERN = r"[ \t]+['\"]?\S*"
79
+ CLAUDE_HOME_TARGET_BOUNDARY_PATTERN = r"[\\/]?['\"]?(?=[\s;&|]|$)"
80
+ VERDICT_DIRECTORY_TARGET_BOUNDARY_PATTERN = r"[\\/]?['\"]?(?=[\s;&|]|$)"
81
+ COMMAND_AFTER_DIRECTORY_CHANGE_PATTERN = r"[;&|\n][\s]*\S"
82
+ OPTION_WITH_VALUE_STEP = 2
83
+ ALL_GATED_TOOL_NAMES = ("Bash", "PowerShell")
84
+ HASH_PREVIEW_LENGTH = 16
85
+ MINTING_AGENT_TYPE = "code-verifier"
86
+ SPAWN_LOOKUP_ATTEMPT_COUNT = 3
87
+ SPAWN_LOOKUP_RETRY_DELAY_SECONDS = 0.1
88
+ VERDICT_DIRECTORY_GUARD_MESSAGE = (
89
+ "BLOCKED: [VERDICT_DIRECTORY_GUARD] Shell access to the verification "
90
+ "verdict directory (~/.claude/verification/) is denied. Only the "
91
+ "verifier_verdict_minter.py SubagentStop hook writes verdict files; a "
92
+ "shell write here would forge a passing verdict and defeat the "
93
+ "verified-commit gate. Spawn the code-verifier agent to earn a verdict "
94
+ "instead of writing one."
95
+ )
96
+ CORRECTIVE_MESSAGE = (
97
+ "BLOCKED: [VERIFIED_COMMIT_GATE] This branch surface has no passing "
98
+ "verification verdict. Spawn the code-verifier agent (Agent tool, "
99
+ "subagent_type 'code-verifier') with the task texts, the diff scope, "
100
+ "and recorded baselines; when it finishes with a clean verdict the "
101
+ "SubagentStop hook mints the verdict and this command will pass. Any "
102
+ "file change after verification invalidates the verdict, so verify "
103
+ "last. Exempt automatically: docs/image files, pytest test files, and "
104
+ "Python files whose docstring- and comment-stripped AST is unchanged "
105
+ "(comment-only edits in non-Python files are not exempt)."
106
+ )