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.
- package/_shared/pr-loop/scripts/code_rules_gate.py +60 -5
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/inline_duplicate_body_span_constants.py +22 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +147 -3
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +3 -2
- package/hooks/blocking/claude_md_orphan_file_blocker.py +170 -20
- package/hooks/blocking/code_rules_duplicate_body.py +378 -26
- package/hooks/blocking/code_rules_enforcer.py +36 -5
- package/hooks/blocking/code_rules_imports_logging.py +679 -1
- package/hooks/blocking/code_rules_shared.py +8 -5
- package/hooks/blocking/code_rules_test_assertions.py +6 -7
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +2 -1
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +155 -0
- package/hooks/blocking/hedging_language_blocker.py +1 -13
- package/hooks/blocking/intent_only_ending_blocker.py +1 -15
- package/hooks/blocking/pre_tool_use_dispatcher.py +4 -5
- package/hooks/blocking/question_to_user_enforcer.py +1 -11
- package/hooks/blocking/session_handoff_blocker.py +1 -15
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +484 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +157 -0
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +466 -0
- package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +11 -9
- package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +758 -0
- package/hooks/blocking/test_code_rules_logging_printf_tokens.py +134 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +16 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +328 -0
- package/hooks/blocking/test_hedging_language_blocker.py +6 -0
- package/hooks/blocking/test_intent_only_ending_blocker.py +5 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +52 -5
- package/hooks/blocking/test_question_to_user_enforcer.py +6 -0
- package/hooks/blocking/test_session_handoff_blocker.py +6 -0
- package/hooks/blocking/test_verification_verdict_store.py +66 -1
- package/hooks/blocking/test_verifier_verdict_minter.py +64 -5
- package/hooks/blocking/verification_verdict_store.py +19 -5
- package/hooks/blocking/verifier_verdict_minter.py +18 -15
- package/hooks/hooks_constants/CLAUDE.md +4 -1
- package/hooks/hooks_constants/blocking_check_limits.py +30 -1
- package/hooks/hooks_constants/claude_md_orphan_file_blocker_constants.py +52 -24
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -1
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +2 -1
- package/hooks/hooks_constants/duplicate_function_body_constants.py +21 -5
- package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +27 -0
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +2 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +8 -2
- package/hooks/hooks_constants/test_post_tool_use_dispatcher_constants.py +43 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +99 -0
- package/hooks/hooks_constants/test_text_stripping.py +39 -0
- package/hooks/hooks_constants/text_stripping.py +36 -0
- package/hooks/validation/CLAUDE.md +1 -0
- package/hooks/validation/post_tool_use_dispatcher.py +2 -2
- package/hooks/validation/test_mypy_validator.py +1 -1
- package/hooks/validation/test_post_tool_use_dispatcher.py +6 -0
- package/hooks/workflow/auto_formatter.py +8 -5
- package/hooks/workflow/test_auto_formatter.py +33 -0
- package/package.json +1 -1
- package/rules/claude-md-orphan-file.md +7 -8
- package/rules/docstring-prose-matches-implementation.md +1 -0
- package/rules/package-inventory-stale-entry.md +8 -0
- package/rules/windows-filesystem-safe.md +2 -0
- package/skills/anthropic-plan/CLAUDE.md +1 -1
- package/skills/anthropic-plan/SKILL.md +15 -2
- package/skills/autoconverge/SKILL.md +6 -3
- package/skills/autoconverge/reference/stop-conditions.md +7 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +5 -4
- package/skills/autoconverge/workflow/converge.contract.test.mjs +306 -137
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +16 -16
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +107 -44
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -24
- package/skills/autoconverge/workflow/converge.mjs +581 -613
- package/skills/autoconverge/workflow/convergence_summary.py +1 -1
- package/skills/autoconverge/workflow/render_report.py +2 -6
- package/skills/autoconverge/workflow/test_convergence_summary.py +17 -0
- package/skills/autoconverge/workflow/test_render_report.py +1 -0
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +58 -4
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +9 -0
- 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[
|
|
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: ``(
|
|
385
|
-
``ast.walk`` traversal order, where ``
|
|
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) ->
|
|
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
|
-
|
|
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
|
|
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 (
|
|
12
|
-
- Edit -> Group A (
|
|
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]:
|