claude-dev-env 1.59.0 → 1.61.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 (81) hide show
  1. package/CLAUDE.md +4 -0
  2. package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +1 -1
  3. package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -0
  4. package/audit-rubrics/category_rubrics/category-f-silent-failures.md +1 -1
  5. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  6. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
  7. package/audit-rubrics/prompts/category-e-dead-code.md +17 -4
  8. package/audit-rubrics/prompts/category-f-silent-failures.md +1 -0
  9. package/docs/CODE_RULES.md +2 -2
  10. package/hooks/blocking/code_rules_annotations_length.py +189 -10
  11. package/hooks/blocking/code_rules_dead_module_constant.py +321 -0
  12. package/hooks/blocking/code_rules_duplicate_body.py +152 -0
  13. package/hooks/blocking/code_rules_enforcer.py +38 -15
  14. package/hooks/blocking/code_rules_orphan_css_class.py +196 -0
  15. package/hooks/blocking/code_rules_typeddict_stub.py +172 -0
  16. package/hooks/blocking/config/__init__.py +5 -0
  17. package/hooks/blocking/config/verified_commit_constants.py +118 -0
  18. package/hooks/blocking/destructive_command_blocker.py +483 -61
  19. package/hooks/blocking/test_code_rules_enforcer_annotations.py +240 -0
  20. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
  21. package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +146 -0
  22. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +188 -0
  23. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +82 -0
  24. package/hooks/blocking/test_code_rules_enforcer_orphan_css_class.py +196 -0
  25. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +415 -0
  26. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +156 -0
  27. package/hooks/blocking/test_destructive_command_blocker.py +213 -0
  28. package/hooks/blocking/test_verdict_directory_write_blocker.py +720 -0
  29. package/hooks/blocking/test_verification_verdict_store.py +490 -0
  30. package/hooks/blocking/test_verified_commit_gate.py +495 -0
  31. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -0
  32. package/hooks/blocking/test_verifier_verdict_minter.py +193 -0
  33. package/hooks/blocking/verdict_directory_write_blocker.py +667 -0
  34. package/hooks/blocking/verification_verdict_store.py +686 -0
  35. package/hooks/blocking/verified_commit_gate.py +535 -0
  36. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +152 -0
  37. package/hooks/blocking/verifier_verdict_minter.py +221 -0
  38. package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
  39. package/hooks/hooks.json +43 -1
  40. package/hooks/hooks_constants/blocking_check_limits.py +1 -0
  41. package/hooks/hooks_constants/code_rules_enforcer_constants.py +6 -0
  42. package/hooks/hooks_constants/dead_module_constant_constants.py +20 -0
  43. package/hooks/hooks_constants/destructive_command_segment_constants.py +15 -0
  44. package/hooks/hooks_constants/duplicate_function_body_constants.py +22 -5
  45. package/hooks/hooks_constants/orphan_css_class_constants.py +40 -0
  46. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
  47. package/hooks/validation/mypy_validator.py +59 -7
  48. package/hooks/validation/test_mypy_validator.py +94 -0
  49. package/package.json +1 -1
  50. package/rules/file-global-constants.md +7 -1
  51. package/rules/no-cross-skill-duplicate-helpers.md +29 -0
  52. package/rules/orphan-css-class.md +23 -0
  53. package/skills/_shared/pr-loop/scripts/preflight_worktree.py +392 -0
  54. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +70 -0
  55. package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +263 -0
  56. package/skills/autoconverge/SKILL.md +54 -17
  57. package/skills/autoconverge/reference/closing-report.md +59 -17
  58. package/skills/autoconverge/workflow/aggregate_runs.py +371 -0
  59. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +192 -76
  60. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +76 -0
  61. package/skills/autoconverge/workflow/converge.contract.test.mjs +395 -206
  62. package/skills/autoconverge/workflow/converge.mjs +520 -57
  63. package/skills/autoconverge/workflow/convergence_summary.py +110 -0
  64. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +2 -0
  65. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +7 -0
  66. package/skills/autoconverge/workflow/render_report.py +488 -397
  67. package/skills/autoconverge/workflow/test_aggregate_runs.py +134 -0
  68. package/skills/autoconverge/workflow/test_convergence_summary.py +132 -0
  69. package/skills/autoconverge/workflow/test_render_report.py +518 -259
  70. package/skills/pr-converge/reference/per-tick.md +28 -8
  71. package/skills/rebase/SKILL.md +2 -4
  72. package/system-prompts/software-engineer.xml +2 -6
  73. package/hooks/blocking/content_search_to_zoekt_redirector.py +0 -59
  74. package/hooks/blocking/content_search_zoekt_bash_block_reason.py +0 -25
  75. package/hooks/blocking/content_search_zoekt_block_payload.py +0 -21
  76. package/hooks/blocking/content_search_zoekt_indexed_paths.py +0 -24
  77. package/hooks/blocking/content_search_zoekt_indexed_roots_config.py +0 -131
  78. package/hooks/blocking/content_search_zoekt_redirect_guidance.py +0 -52
  79. package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +0 -61
  80. package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +0 -92
  81. package/hooks/blocking/test_content_search_zoekt_indexed_roots_config.py +0 -102
@@ -0,0 +1,196 @@
1
+ """Orphan-CSS-class check: class attributes in markup with no matching selector."""
2
+
3
+ import ast
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ _blocking_directory = str(Path(__file__).resolve().parent)
8
+ _hooks_directory = str(Path(__file__).resolve().parent.parent)
9
+ if _blocking_directory not in sys.path:
10
+ sys.path.insert(0, _blocking_directory)
11
+ if _hooks_directory not in sys.path:
12
+ sys.path.insert(0, _hooks_directory)
13
+
14
+ from code_rules_shared import ( # noqa: E402
15
+ is_test_file,
16
+ )
17
+
18
+ from hooks_constants.orphan_css_class_constants import ( # noqa: E402
19
+ CLASS_ATTRIBUTE_PATTERN,
20
+ CSS_CLASS_SELECTOR_PATTERN,
21
+ MAX_ORPHAN_CSS_CLASS_ISSUES,
22
+ MAX_SIBLING_MODULES_SCANNED,
23
+ ORPHAN_CSS_CLASS_MESSAGE_SUFFIX,
24
+ PYTHON_MODULE_GLOB,
25
+ STYLE_BLOCK_PATTERN,
26
+ )
27
+
28
+
29
+ def _string_literals_with_lines(tree: ast.Module) -> list[tuple[str, int]]:
30
+ """Return every string-constant value in the tree paired with its line number.
31
+
32
+ Args:
33
+ tree: The parsed module to walk.
34
+
35
+ Returns:
36
+ A list of ``(string_value, line_number)`` pairs, one per string constant.
37
+ """
38
+ literals: list[tuple[str, int]] = []
39
+ for each_node in ast.walk(tree):
40
+ if isinstance(each_node, ast.Constant) and isinstance(each_node.value, str):
41
+ literals.append((each_node.value, each_node.lineno))
42
+ return literals
43
+
44
+
45
+ def _class_names_in_attribute(attribute_text: str) -> list[str]:
46
+ """Return the individual class names in a single ``class="..."`` attribute.
47
+
48
+ Args:
49
+ attribute_text: The whitespace-separated class list from one attribute.
50
+
51
+ Returns:
52
+ Each non-empty class token, in order.
53
+ """
54
+ return [each_token for each_token in attribute_text.split() if each_token]
55
+
56
+
57
+ def _class_references_with_lines(
58
+ all_string_literals: list[tuple[str, int]],
59
+ ) -> list[tuple[str, int]]:
60
+ """Return every class name referenced in a ``class="..."`` attribute.
61
+
62
+ Args:
63
+ all_string_literals: The ``(literal_text, line_number)`` constants to scan.
64
+
65
+ Returns:
66
+ A list of ``(class_name, line_number)`` pairs, one per referenced class.
67
+ """
68
+ references: list[tuple[str, int]] = []
69
+ for each_text, each_line in all_string_literals:
70
+ for each_match in CLASS_ATTRIBUTE_PATTERN.finditer(each_text):
71
+ for each_class_name in _class_names_in_attribute(each_match.group(1)):
72
+ references.append((each_class_name, each_line))
73
+ return references
74
+
75
+
76
+ def _defined_class_selectors(all_string_literals: list[tuple[str, int]]) -> set[str]:
77
+ """Return every CSS class name defined by a selector inside a ``<style>`` block.
78
+
79
+ Args:
80
+ all_string_literals: The ``(literal_text, line_number)`` constants to scan.
81
+
82
+ Returns:
83
+ The set of class names that carry a matching ``.<class>`` selector.
84
+ """
85
+ defined: set[str] = set()
86
+ for each_text, _ in all_string_literals:
87
+ for each_style_match in STYLE_BLOCK_PATTERN.finditer(each_text):
88
+ for each_selector in CSS_CLASS_SELECTOR_PATTERN.finditer(
89
+ each_style_match.group(1)
90
+ ):
91
+ defined.add(each_selector.group(1))
92
+ return defined
93
+
94
+
95
+ def _sibling_module_paths(file_path: str) -> list[Path]:
96
+ """Return the importable sibling Python modules near *file_path*.
97
+
98
+ Scans the file's own directory and its immediate child directories, since a
99
+ markup module commonly imports its ``<style>`` constant from a companion
100
+ package directory beside it. The scan is bounded so a large tree never
101
+ stalls a write.
102
+
103
+ Args:
104
+ file_path: The absolute path of the file under validation.
105
+
106
+ Returns:
107
+ The sibling ``.py`` paths to read for cross-module selector resolution,
108
+ excluding the file itself, capped at the scan budget.
109
+ """
110
+ target = Path(file_path)
111
+ base_directory = target.parent
112
+ if not base_directory.is_dir():
113
+ return []
114
+ siblings: list[Path] = []
115
+ for each_path in sorted(base_directory.rglob(PYTHON_MODULE_GLOB)):
116
+ if each_path.resolve() == target.resolve():
117
+ continue
118
+ siblings.append(each_path)
119
+ if len(siblings) >= MAX_SIBLING_MODULES_SCANNED:
120
+ break
121
+ return siblings
122
+
123
+
124
+ def _selectors_from_sibling_modules(file_path: str) -> set[str]:
125
+ """Return CSS class selectors defined in ``<style>`` blocks of sibling modules.
126
+
127
+ Args:
128
+ file_path: The absolute path of the file under validation.
129
+
130
+ Returns:
131
+ The union of class names whose selectors appear in any readable sibling
132
+ module's string literals.
133
+ """
134
+ selectors: set[str] = set()
135
+ for each_sibling in _sibling_module_paths(file_path):
136
+ try:
137
+ sibling_source = each_sibling.read_text(encoding="utf-8")
138
+ except (OSError, UnicodeDecodeError):
139
+ continue
140
+ try:
141
+ sibling_tree = ast.parse(sibling_source)
142
+ except SyntaxError:
143
+ continue
144
+ selectors |= _defined_class_selectors(_string_literals_with_lines(sibling_tree))
145
+ return selectors
146
+
147
+
148
+ def check_orphan_css_classes(content: str, file_path: str) -> list[str]:
149
+ """Flag ``class="..."`` markup whose class has no matching CSS selector.
150
+
151
+ A module that emits HTML names each class it references with a matching
152
+ ``.<class>`` selector, either in a ``<style>`` block in the same file or in
153
+ a companion module beside it. A referenced class with no selector anywhere
154
+ is a dead attribute (or a missing rule), so this flags it. The check only
155
+ fires for a file that itself emits markup, and only after a ``<style>``
156
+ block exists in the file or a sibling — a file with markup but no style
157
+ source nearby is left alone, since its stylesheet lives outside the scan.
158
+ Test files are exempt, since a fixture may carry intentional orphan markup.
159
+
160
+ Args:
161
+ content: The new or whole-file content being written.
162
+ file_path: The destination path of the write or edit.
163
+
164
+ Returns:
165
+ One issue per orphan class reference, capped at the issue budget.
166
+ """
167
+ if is_test_file(file_path):
168
+ return []
169
+ try:
170
+ tree = ast.parse(content)
171
+ except SyntaxError:
172
+ return []
173
+ all_string_literals = _string_literals_with_lines(tree)
174
+ class_references = _class_references_with_lines(all_string_literals)
175
+ if not class_references:
176
+ return []
177
+ defined_selectors = _defined_class_selectors(all_string_literals)
178
+ defined_selectors |= _selectors_from_sibling_modules(file_path)
179
+ if not defined_selectors:
180
+ return []
181
+ issues: list[str] = []
182
+ reported_classes: set[str] = set()
183
+ for each_class_name, each_line in class_references:
184
+ if each_class_name in defined_selectors:
185
+ continue
186
+ if each_class_name in reported_classes:
187
+ continue
188
+ reported_classes.add(each_class_name)
189
+ issues.append(
190
+ f"Line {each_line}: CSS class {each_class_name!r} used in markup"
191
+ f" has no matching '.{each_class_name}' selector - "
192
+ f"{ORPHAN_CSS_CLASS_MESSAGE_SUFFIX}"
193
+ )
194
+ if len(issues) >= MAX_ORPHAN_CSS_CLASS_ISSUES:
195
+ break
196
+ return issues
@@ -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,118 @@
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
+ VERDICT_KEY_FINDINGS = "findings"
51
+ SUBAGENTS_DIRECTORY_NAME = "subagents"
52
+ AGENT_TRANSCRIPT_GLOB = "agent-*.jsonl"
53
+ AGENT_META_SIDECAR_SUFFIX = ".meta.json"
54
+ AGENT_META_TYPE_KEY = "agentType"
55
+ TRANSCRIPT_ENTRY_TYPE_KEY = "type"
56
+ TRANSCRIPT_ASSISTANT_ENTRY_TYPE = "assistant"
57
+ TRANSCRIPT_MESSAGE_KEY = "message"
58
+ TRANSCRIPT_CONTENT_KEY = "content"
59
+ TRANSCRIPT_CONTENT_TYPE_KEY = "type"
60
+ TRANSCRIPT_TEXT_CONTENT_TYPE = "text"
61
+ TRANSCRIPT_TEXT_KEY = "text"
62
+ VERDICT_FENCE_PATTERN = r"```verdict\s*\n(.*?)```"
63
+ MANIFEST_HASH_CLI_FLAG = "--manifest-hash"
64
+ DOCS_ONLY_EXTENSIONS = frozenset(
65
+ {".md", ".txt", ".rst", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico"}
66
+ )
67
+ PYTHON_EXTENSION = ".py"
68
+ TEST_FILE_PREFIX = "test_"
69
+ TEST_FILE_SUFFIX = "_test.py"
70
+ CONFTEST_FILE_NAME = "conftest.py"
71
+ MINIMUM_STATUS_FIELD_COUNT = 2
72
+ ALL_FALLBACK_BASE_REFERENCES = ("origin/main", "origin/master")
73
+ ALL_TOOLING_STATE_PREFIXES = (
74
+ ".claude/verification/",
75
+ ".claude/worktrees/",
76
+ ".claude/daemon/",
77
+ ".claude/teams/",
78
+ ".claude/sessions/",
79
+ ".cursor/worktrees/",
80
+ )
81
+ GATED_GIT_SUBCOMMANDS = frozenset({"commit", "push"})
82
+ ALL_GIT_BINARY_NAMES = frozenset({"git", "git.exe"})
83
+ VALUE_TAKING_GIT_OPTIONS = frozenset({"-C", "-c", "--git-dir", "--work-tree", "--namespace"})
84
+ REPO_DIRECTORY_OPTION = "-C"
85
+ WORK_TREE_OPTION = "--work-tree"
86
+ DIRECTORY_CHANGE_VERBS = frozenset({"cd", "pushd", "set-location", "sl"})
87
+ DIRECTORY_CHANGE_PATH_OPTIONS = frozenset({"-path", "-literalpath"})
88
+ DIRECTORY_CHANGE_OPTION_TERMINATOR = "--"
89
+ DIRECTORY_CHANGE_PATTERN_PREFIX = r"(?:^|(?<=[\s;&|(]))(?:"
90
+ DIRECTORY_CHANGE_PATTERN_SUFFIX = r")(?=\s|$)"
91
+ DIRECTORY_CHANGE_OPTION_PREFIX_PATTERN = r"(?:[ \t]+(?:%s)(?=\s|$))*"
92
+ DIRECTORY_CHANGE_TARGET_PATTERN = r"[ \t]+['\"]?\S*"
93
+ CLAUDE_HOME_TARGET_BOUNDARY_PATTERN = r"[\\/]?['\"]?(?=[\s;&|]|$)"
94
+ VERDICT_DIRECTORY_TARGET_BOUNDARY_PATTERN = r"[\\/]?['\"]?(?=[\s;&|]|$)"
95
+ COMMAND_AFTER_DIRECTORY_CHANGE_PATTERN = r"[;&|\n][\s]*\S"
96
+ OPTION_WITH_VALUE_STEP = 2
97
+ ALL_GATED_TOOL_NAMES = ("Bash", "PowerShell")
98
+ HASH_PREVIEW_LENGTH = 16
99
+ MINTING_AGENT_TYPE = "code-verifier"
100
+ VERDICT_DIRECTORY_GUARD_MESSAGE = (
101
+ "BLOCKED: [VERDICT_DIRECTORY_GUARD] Shell access to the verification "
102
+ "verdict directory (~/.claude/verification/) is denied. Only the "
103
+ "verifier_verdict_minter.py SubagentStop hook writes verdict files; a "
104
+ "shell write here would forge a passing verdict and defeat the "
105
+ "verified-commit gate. Spawn the code-verifier agent to earn a verdict "
106
+ "instead of writing one."
107
+ )
108
+ CORRECTIVE_MESSAGE = (
109
+ "BLOCKED: [VERIFIED_COMMIT_GATE] This branch surface has no passing "
110
+ "verification verdict. Spawn the code-verifier agent (Agent tool, "
111
+ "subagent_type 'code-verifier') with the task texts, the diff scope, "
112
+ "and recorded baselines; when it finishes with a clean verdict the "
113
+ "SubagentStop hook mints the verdict and this command will pass. Any "
114
+ "file change after verification invalidates the verdict, so verify "
115
+ "last. Exempt automatically: docs/image files, pytest test files, and "
116
+ "Python files whose docstring- and comment-stripped AST is unchanged "
117
+ "(comment-only edits in non-Python files are not exempt)."
118
+ )