claude-dev-env 1.74.0 → 1.76.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 (78) hide show
  1. package/_shared/pr-loop/scripts/code_rules_gate.py +60 -5
  2. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
  3. package/_shared/pr-loop/scripts/pr_loop_shared_constants/inline_duplicate_body_span_constants.py +22 -0
  4. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +147 -3
  5. package/docs/CODE_RULES.md +1 -1
  6. package/hooks/blocking/CLAUDE.md +3 -2
  7. package/hooks/blocking/claude_md_orphan_file_blocker.py +170 -20
  8. package/hooks/blocking/code_rules_duplicate_body.py +378 -26
  9. package/hooks/blocking/code_rules_enforcer.py +36 -5
  10. package/hooks/blocking/code_rules_imports_logging.py +679 -1
  11. package/hooks/blocking/code_rules_shared.py +8 -5
  12. package/hooks/blocking/code_rules_test_assertions.py +6 -7
  13. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +2 -1
  14. package/hooks/blocking/duplicate_rmtree_helper_blocker.py +155 -0
  15. package/hooks/blocking/hedging_language_blocker.py +1 -13
  16. package/hooks/blocking/intent_only_ending_blocker.py +1 -15
  17. package/hooks/blocking/pre_tool_use_dispatcher.py +4 -5
  18. package/hooks/blocking/question_to_user_enforcer.py +1 -11
  19. package/hooks/blocking/session_handoff_blocker.py +1 -15
  20. package/hooks/blocking/test_claude_md_orphan_file_blocker.py +484 -0
  21. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
  22. package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +157 -0
  23. package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +466 -0
  24. package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +11 -9
  25. package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +758 -0
  26. package/hooks/blocking/test_code_rules_logging_printf_tokens.py +134 -0
  27. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +16 -0
  28. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +328 -0
  29. package/hooks/blocking/test_hedging_language_blocker.py +6 -0
  30. package/hooks/blocking/test_intent_only_ending_blocker.py +5 -0
  31. package/hooks/blocking/test_pre_tool_use_dispatcher.py +52 -5
  32. package/hooks/blocking/test_question_to_user_enforcer.py +6 -0
  33. package/hooks/blocking/test_session_handoff_blocker.py +6 -0
  34. package/hooks/blocking/test_verification_verdict_store.py +66 -1
  35. package/hooks/blocking/test_verifier_verdict_minter.py +64 -5
  36. package/hooks/blocking/verification_verdict_store.py +19 -5
  37. package/hooks/blocking/verifier_verdict_minter.py +18 -15
  38. package/hooks/hooks_constants/CLAUDE.md +4 -1
  39. package/hooks/hooks_constants/blocking_check_limits.py +30 -1
  40. package/hooks/hooks_constants/claude_md_orphan_file_blocker_constants.py +52 -24
  41. package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -1
  42. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +2 -1
  43. package/hooks/hooks_constants/duplicate_function_body_constants.py +21 -5
  44. package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +27 -0
  45. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +2 -0
  46. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +8 -2
  47. package/hooks/hooks_constants/test_post_tool_use_dispatcher_constants.py +43 -0
  48. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +99 -0
  49. package/hooks/hooks_constants/test_text_stripping.py +39 -0
  50. package/hooks/hooks_constants/text_stripping.py +36 -0
  51. package/hooks/validation/CLAUDE.md +1 -0
  52. package/hooks/validation/post_tool_use_dispatcher.py +2 -2
  53. package/hooks/validation/test_mypy_validator.py +1 -1
  54. package/hooks/validation/test_post_tool_use_dispatcher.py +6 -0
  55. package/hooks/workflow/auto_formatter.py +8 -5
  56. package/hooks/workflow/test_auto_formatter.py +33 -0
  57. package/package.json +1 -1
  58. package/rules/claude-md-orphan-file.md +7 -8
  59. package/rules/docstring-prose-matches-implementation.md +1 -0
  60. package/rules/package-inventory-stale-entry.md +8 -0
  61. package/rules/windows-filesystem-safe.md +2 -0
  62. package/skills/anthropic-plan/CLAUDE.md +1 -1
  63. package/skills/anthropic-plan/SKILL.md +15 -2
  64. package/skills/autoconverge/SKILL.md +6 -3
  65. package/skills/autoconverge/reference/stop-conditions.md +7 -0
  66. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +5 -4
  67. package/skills/autoconverge/workflow/converge.contract.test.mjs +306 -137
  68. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +16 -16
  69. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +107 -44
  70. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -24
  71. package/skills/autoconverge/workflow/converge.mjs +581 -613
  72. package/skills/autoconverge/workflow/convergence_summary.py +1 -1
  73. package/skills/autoconverge/workflow/render_report.py +2 -6
  74. package/skills/autoconverge/workflow/test_convergence_summary.py +17 -0
  75. package/skills/autoconverge/workflow/test_render_report.py +1 -0
  76. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +58 -4
  77. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +9 -0
  78. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +42 -0
@@ -4,7 +4,7 @@ import ast
4
4
  import difflib
5
5
  import os
6
6
  import sys
7
- from collections.abc import Iterator
7
+ from collections.abc import Collection, Iterator
8
8
  from pathlib import Path
9
9
 
10
10
  _blocking_directory = str(Path(__file__).resolve().parent)
@@ -360,7 +360,7 @@ def changed_line_numbers(prior_content: str, post_edit_content: str) -> set[int]
360
360
 
361
361
 
362
362
  def _scope_violations_to_changed_lines(
363
- all_violations_in_walk_order: list[tuple[range, str]],
363
+ all_violations_in_walk_order: list[tuple[Collection[int], str]],
364
364
  all_changed_lines: set[int] | None,
365
365
  defer_scope_to_caller: bool = False,
366
366
  ) -> list[str]:
@@ -381,9 +381,12 @@ def _scope_violations_to_changed_lines(
381
381
  block a single-file edit.
382
382
 
383
383
  Args:
384
- all_violations_in_walk_order: ``(span_range, issue_message)`` pairs in
385
- ``ast.walk`` traversal order, where ``span_range`` covers the
386
- violation's source lines.
384
+ all_violations_in_walk_order: ``(line_collection, issue_message)`` pairs
385
+ in ``ast.walk`` traversal order, where ``line_collection`` holds the
386
+ violation's source lines. A whole-function violation passes a
387
+ contiguous ``range``; a two-function violation passes the union of
388
+ both functions' lines, so the lines between the two functions stay
389
+ out of scope and an edit to only that gap does not block.
387
390
  all_changed_lines: Post-edit line numbers the current edit touched, or
388
391
  None to treat every violation as in-scope.
389
392
  defer_scope_to_caller: When True, return every violation message in walk
@@ -286,7 +286,7 @@ def _name_encodes_scenario(test_name: str) -> bool:
286
286
  return any(each_clause in test_name for each_clause in _SCENARIO_NAME_CLAUSES)
287
287
 
288
288
 
289
- def check_flag_gated_scenario_test_naming(content: str, file_path: str) -> list[str]:
289
+ def check_flag_gated_scenario_test_naming(content: str, file_path: str) -> None:
290
290
  """Flag a scenario-named test that omits a flag its siblings establish.
291
291
 
292
292
  When two or more sibling tests in a file monkeypatch the same module-level
@@ -302,15 +302,16 @@ def check_flag_gated_scenario_test_naming(content: str, file_path: str) -> list[
302
302
  file_path: Path to the file, used for the test-file gate.
303
303
 
304
304
  Returns:
305
- An empty list; advisories print to stderr so the write proceeds.
305
+ None; advisories print to stderr and this check never contributes to
306
+ the blocking issue list, so the write proceeds.
306
307
  """
307
308
  if not is_test_file(file_path):
308
- return []
309
+ return
309
310
 
310
311
  try:
311
312
  syntax_tree = ast.parse(content)
312
313
  except SyntaxError:
313
- return []
314
+ return
314
315
 
315
316
  test_functions = [
316
317
  each_node
@@ -333,7 +334,7 @@ def check_flag_gated_scenario_test_naming(content: str, file_path: str) -> list[
333
334
  if patch_count >= _MINIMUM_SIBLING_PATCH_COUNT
334
335
  }
335
336
  if not established_flags:
336
- return []
337
+ return
337
338
 
338
339
  for each_test in test_functions:
339
340
  if not _name_encodes_scenario(each_test.name):
@@ -349,8 +350,6 @@ def check_flag_gated_scenario_test_naming(content: str, file_path: str) -> list[
349
350
  file=sys.stderr,
350
351
  )
351
352
 
352
- return []
353
-
354
353
 
355
354
  def _called_function_names(function_node: ast.FunctionDef | ast.AsyncFunctionDef) -> set[str]:
356
355
  """Return the bare names of every function the test body calls."""
@@ -5,7 +5,8 @@ The hook fires only on an ``Agent`` tool call whose ``subagent_type`` is
5
5
  ``code-verifier``. Before that verification spawn runs, the hook checks the
6
6
  branch for two committability problems against the resolved base ref: a real
7
7
  merge conflict (a non-mutating trial-merge of HEAD against the base ref) and a
8
- CODE_RULES violation on a line added in the uncommitted working tree. When
8
+ CODE_RULES violation on a line added in the working tree since the merge base
9
+ (committed on the branch or uncommitted). When
9
10
  either fires, the hook denies the spawn with a reason addressed to the spawning
10
11
  agent that names the conflicting files and the violating file:line, so that
11
12
  agent fixes them and re-spawns. Both checks fail OPEN on any infrastructure
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env python3
2
+ """PreToolUse hook: block a local re-definition of the Windows-safe rmtree helper trio.
3
+
4
+ The Windows-safe deletion helper trio — `_strip_read_only_and_retry`,
5
+ `_force_remove_tree`/`force_rmtree`, and the `inspect.signature` onexc/onerror guard —
6
+ is the sanctioned pattern for removing a directory tree that may hold ReadOnly files.
7
+ Because the windows_rmtree_blocker corrective message ships the trio as a paste-ready
8
+ snippet, agents paste a fresh local copy into each module that needs cleanup. Three
9
+ near-matching copies already span one codebase (a parser service, a categorizer, and a
10
+ test isolation helper), so a fix to one copy never reaches the others — the exact
11
+ "duplicated logic drifts" failure CODE_RULES.md section 3 (Reuse before create) names.
12
+
13
+ This hook scans Write/Edit content to a Python file for a `def` of any sanctioned
14
+ helper name and blocks it with a corrective message pointing to a single shared
15
+ force_rmtree utility. The canonical shared-helper home, the rmtree-blocker hook
16
+ sources (whose corrective strings embed the snippet), and test files are exempt.
17
+
18
+ This complements the same-directory `check_duplicate_function_body_across_files`
19
+ gate, which compares a written function only against `.py` siblings in its own
20
+ directory. That scope leaves a copy of this trio between two distant packages
21
+ unguarded, which is how the copies above spread. Keying on the sanctioned helper
22
+ names blocks the cross-directory copy the structural same-directory check cannot
23
+ reach.
24
+ """
25
+
26
+ import json
27
+ import sys
28
+ from pathlib import Path
29
+
30
+ _hooks_dir = str(Path(__file__).resolve().parent.parent)
31
+ if _hooks_dir not in sys.path:
32
+ sys.path.insert(0, _hooks_dir)
33
+
34
+ from hooks_constants.duplicate_rmtree_helper_blocker_constants import ( # noqa: E402
35
+ ALL_EXEMPT_PATH_FRAGMENTS,
36
+ ALL_EXEMPT_TEST_FILE_PREFIXES,
37
+ ALL_EXEMPT_TEST_FILE_SUFFIXES,
38
+ HELPER_DEFINITION_PATTERN,
39
+ PYTHON_FILE_EXTENSION,
40
+ TRIPLE_QUOTED_STRING_PATTERN,
41
+ )
42
+ from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin # noqa: E402
43
+
44
+
45
+ def payload_defines_sanctioned_helper(payload_text: str) -> bool:
46
+ """Return True when the text defines a sanctioned Windows-safe rmtree helper.
47
+
48
+ Args:
49
+ payload_text: The file content or new_string fragment under inspection.
50
+
51
+ Returns:
52
+ True when a line defines `_strip_read_only_and_retry`, `_force_remove_tree`,
53
+ or `force_rmtree`. Triple-quoted string bodies are masked before the
54
+ line-anchored pattern runs, so a `def` that begins its own line inside a
55
+ documentation snippet or multi-line string literal is left untouched. A
56
+ helper name inside a single-line quoted string carries a quote before `def`,
57
+ so the line-anchored pattern leaves it untouched as well.
58
+ """
59
+ if not payload_text:
60
+ return False
61
+ masked_text = TRIPLE_QUOTED_STRING_PATTERN.sub("", payload_text)
62
+ return bool(HELPER_DEFINITION_PATTERN.search(masked_text))
63
+
64
+
65
+ def path_is_exempt(file_path: str) -> bool:
66
+ """Return True when a Python path may carry the helper definition.
67
+
68
+ Args:
69
+ file_path: The target path the Write/Edit writes to.
70
+
71
+ Returns:
72
+ True when the path's basename is the canonical shared-helper home, an
73
+ rmtree-blocker hook source, one of the existing in-repo definition sites
74
+ (session_env_cleanup.py, _md_to_html_blocker_test_support.py,
75
+ teardown_worktrees.py), or a test file. A definition there is intentional.
76
+ Basename equality (not substring containment) prevents a sibling whose name
77
+ merely contains an exempt fragment from bypassing the block.
78
+ """
79
+ normalized_path = file_path.replace("\\", "/")
80
+ file_name = normalized_path.rsplit("/", 1)[-1]
81
+ if any(file_name.startswith(each_prefix) for each_prefix in ALL_EXEMPT_TEST_FILE_PREFIXES):
82
+ return True
83
+ if any(file_name.endswith(each_suffix) for each_suffix in ALL_EXEMPT_TEST_FILE_SUFFIXES):
84
+ return True
85
+ return file_name in ALL_EXEMPT_PATH_FRAGMENTS
86
+
87
+
88
+ def extract_payload_text(tool_name: str, tool_input: dict) -> tuple[str, str]:
89
+ """Return the (file_path, scanned_text) pair for a Write/Edit to a Python file.
90
+
91
+ Args:
92
+ tool_name: The PreToolUse tool name.
93
+ tool_input: The tool input dictionary.
94
+
95
+ Returns:
96
+ A pair of the target path and the text to scan. The text is empty for an
97
+ unrelated tool or a non-Python target, so the caller exits without blocking.
98
+ """
99
+ if tool_name not in {"Write", "Edit"}:
100
+ return "", ""
101
+ file_path = tool_input.get("file_path", "") or ""
102
+ if file_path and not file_path.endswith(PYTHON_FILE_EXTENSION):
103
+ return file_path, ""
104
+ scanned_text = tool_input.get("content", "") or tool_input.get("new_string", "") or ""
105
+ return file_path, scanned_text
106
+
107
+
108
+ def main() -> None:
109
+ corrective_message = (
110
+ "BLOCKED [duplicate-rmtree-helper]: this Write/Edit defines a local copy of "
111
+ "the Windows-safe rmtree helper trio (_strip_read_only_and_retry, "
112
+ "_force_remove_tree / force_rmtree). The trio is already implemented once; a "
113
+ "second copy drifts from the original — a fix lands in one copy and the other "
114
+ "keeps the bug (CODE_RULES.md section 3, Reuse before create).\n\n"
115
+ "Import the shared force_rmtree helper rather than pasting the trio:\n\n"
116
+ " from <shared_package>.windows_filesystem import force_rmtree\n"
117
+ " force_rmtree(staging_directory)\n\n"
118
+ "When no shared helper module exists yet, create ONE — a windows-filesystem "
119
+ "utility module the consuming packages can import — define the trio there once, "
120
+ "and import it from every call site. Do not paste the trio from the "
121
+ "windows_rmtree_blocker corrective message into each module.\n\n"
122
+ "See ~/.claude/rules/windows-filesystem-safe.md for the sanctioned pattern."
123
+ )
124
+ hook_input = read_hook_input_dictionary_from_stdin()
125
+ if hook_input is None:
126
+ sys.exit(0)
127
+
128
+ raw_tool_name = hook_input.get("tool_name", "")
129
+ raw_tool_input = hook_input.get("tool_input", {})
130
+ tool_name = raw_tool_name if isinstance(raw_tool_name, str) else ""
131
+ tool_input = raw_tool_input if isinstance(raw_tool_input, dict) else {}
132
+
133
+ file_path, scanned_text = extract_payload_text(tool_name, tool_input)
134
+
135
+ if not scanned_text:
136
+ sys.exit(0)
137
+ if path_is_exempt(file_path):
138
+ sys.exit(0)
139
+ if not payload_defines_sanctioned_helper(scanned_text):
140
+ sys.exit(0)
141
+
142
+ deny_response = {
143
+ "hookSpecificOutput": {
144
+ "hookEventName": "PreToolUse",
145
+ "permissionDecision": "deny",
146
+ "permissionDecisionReason": corrective_message,
147
+ }
148
+ }
149
+ print(json.dumps(deny_response))
150
+ sys.stdout.flush()
151
+ sys.exit(0)
152
+
153
+
154
+ if __name__ == "__main__":
155
+ main()
@@ -18,6 +18,7 @@ if _hooks_dir not in sys.path:
18
18
 
19
19
  from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
20
20
  from hooks_constants.messages import USER_FACING_NOTICE # noqa: E402
21
+ from hooks_constants.text_stripping import strip_code_and_quotes # noqa: E402
21
22
 
22
23
  PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
23
24
 
@@ -58,19 +59,6 @@ ALL_HEDGING_PATTERNS = [
58
59
  re.compile(pattern, re.IGNORECASE) for pattern in HEDGING_WORDS + HEDGING_PHRASES
59
60
  ]
60
61
 
61
- CODE_BLOCK_PATTERN = re.compile(r"```[\s\S]*?```", re.MULTILINE)
62
- INLINE_CODE_PATTERN = re.compile(r"`[^`]+`")
63
- QUOTED_BLOCK_PATTERN = re.compile(r"^>.*$", re.MULTILINE)
64
-
65
-
66
- def strip_code_and_quotes(text: str) -> str:
67
- """Remove code blocks, inline code, and blockquotes to avoid false positives."""
68
- text = CODE_BLOCK_PATTERN.sub("", text)
69
- text = INLINE_CODE_PATTERN.sub("", text)
70
- text = QUOTED_BLOCK_PATTERN.sub("", text)
71
- return text
72
-
73
-
74
62
  def find_hedging_words(text: str) -> list[str]:
75
63
  """Return all hedging words/phrases found in the text."""
76
64
  prose_text = strip_code_and_quotes(text)
@@ -20,21 +20,7 @@ if _hooks_dir not in sys.path:
20
20
 
21
21
  from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
22
22
  from hooks_constants.messages import USER_FACING_INTENT_ENDING_NOTICE # noqa: E402
23
-
24
-
25
- def strip_code_and_quotes(text: str) -> str:
26
- """Remove code blocks, inline code, and blockquotes to avoid false positives.
27
-
28
- Args:
29
- text: The raw assistant message to clean.
30
- """
31
- code_block_pattern = re.compile(r"```[\s\S]*?```", re.MULTILINE)
32
- inline_code_pattern = re.compile(r"`[^`]+`")
33
- quoted_block_pattern = re.compile(r"^>.*$", re.MULTILINE)
34
- text = code_block_pattern.sub("", text)
35
- text = inline_code_pattern.sub("", text)
36
- text = quoted_block_pattern.sub("", text)
37
- return text
23
+ from hooks_constants.text_stripping import strip_code_and_quotes # noqa: E402
38
24
 
39
25
 
40
26
  def extract_final_paragraph(text: str) -> str:
@@ -8,8 +8,8 @@ decision when any hook denied (carrying every denying reason) or exits zero to
8
8
  allow.
9
9
 
10
10
  The per-hook coverage matrix:
11
- - Write -> Group A (10 hooks) + Group B (7 hooks) = 17 hooks
12
- - Edit -> Group A (10 hooks) + Group B (7 hooks) = 17 hooks
11
+ - Write -> Group A (11 hooks) + Group B (7 hooks) = 18 hooks
12
+ - Edit -> Group A (11 hooks) + Group B (7 hooks) = 18 hooks
13
13
  - MultiEdit -> Group B only (7 hooks)
14
14
  """
15
15
 
@@ -40,6 +40,7 @@ from state_description_blocker import evaluate as evaluate_state_description #
40
40
  from hooks_constants.pre_tool_use_dispatcher_constants import ( # noqa: E402
41
41
  ALL_HOSTED_HOOK_ENTRIES,
42
42
  ALLOW_DECISION,
43
+ BLOCKING_CRASH_DENY_REASON,
43
44
  BLOCKING_CRASH_EXIT_CODE,
44
45
  DENY_DECISION,
45
46
  EXIT_CODE_TWO_DENY_REASON,
@@ -360,9 +361,7 @@ def aggregate_hosted_hook_results(
360
361
  parsed_output.deny_reason if parsed_output.deny_reason else EXIT_CODE_TWO_DENY_REASON
361
362
  )
362
363
  elif each_result.did_crash and each_result.is_blocking:
363
- all_deny_reasons.append(
364
- "[dispatcher] hook crash in blocking hook — write blocked for safety"
365
- )
364
+ all_deny_reasons.append(BLOCKING_CRASH_DENY_REASON)
366
365
  elif each_result.exit_code == BLOCKING_CRASH_EXIT_CODE and each_result.is_blocking:
367
366
  all_deny_reasons.append(EXIT_CODE_TWO_DENY_REASON)
368
367
  if parsed_output.is_allow:
@@ -21,17 +21,7 @@ if _hooks_dir not in sys.path:
21
21
 
22
22
  from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
23
23
  from hooks_constants.messages import USER_FACING_ASKUSERQUESTION_NOTICE # noqa: E402
24
-
25
-
26
- def strip_code_and_quotes(text: str) -> str:
27
- """Remove code blocks, inline code, and blockquotes to avoid false positives."""
28
- code_block_pattern = re.compile(r"```[\s\S]*?```", re.MULTILINE)
29
- inline_code_pattern = re.compile(r"`[^`]+`")
30
- quoted_block_pattern = re.compile(r"^>.*$", re.MULTILINE)
31
- text = code_block_pattern.sub("", text)
32
- text = inline_code_pattern.sub("", text)
33
- text = quoted_block_pattern.sub("", text)
34
- return text
24
+ from hooks_constants.text_stripping import strip_code_and_quotes # noqa: E402
35
25
 
36
26
 
37
27
  def extract_final_paragraph(text: str) -> str:
@@ -23,21 +23,7 @@ from hooks_constants.messages import USER_FACING_CONTEXT_REASSURANCE_NOTICE # n
23
23
  from hooks_constants.session_handoff_blocker_constants import ( # noqa: E402
24
24
  FIRST_PERSON_SUBJECT_PATTERN,
25
25
  )
26
-
27
-
28
- def strip_code_and_quotes(text: str) -> str:
29
- """Remove code blocks, inline code, and blockquotes to avoid false positives.
30
-
31
- Args:
32
- text: The raw assistant message to clean.
33
- """
34
- code_block_pattern = re.compile(r"```[\s\S]*?```", re.MULTILINE)
35
- inline_code_pattern = re.compile(r"`[^`]+`")
36
- quoted_block_pattern = re.compile(r"^>.*$", re.MULTILINE)
37
- text = code_block_pattern.sub("", text)
38
- text = inline_code_pattern.sub("", text)
39
- text = quoted_block_pattern.sub("", text)
40
- return text
26
+ from hooks_constants.text_stripping import strip_code_and_quotes # noqa: E402
41
27
 
42
28
 
43
29
  def split_into_sentences(text: str) -> list[str]: