claude-dev-env 8.66.0 → 8.67.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.
|
@@ -17,6 +17,13 @@ It denies a colon that joins two clauses on one line, and an em dash::
|
|
|
17
17
|
ok: Two checks failed at 9:47, then the list follows:
|
|
18
18
|
(each item on its own line below the colon)
|
|
19
19
|
|
|
20
|
+
It denies a causal sentence ("so", "because", "caused", "due to",
|
|
21
|
+
"therefore", "means") that carries no inline code, URL, or markdown link
|
|
22
|
+
of its own. A command block in the same reply is no evidence::
|
|
23
|
+
|
|
24
|
+
flag: That window was Windows PowerShell without Administrator, so the write did not land.
|
|
25
|
+
ok: The write did not land, because the next read printed `Right after write: 1`.
|
|
26
|
+
|
|
20
27
|
It denies a banned word or phrase, matched whole and case-insensitive::
|
|
21
28
|
|
|
22
29
|
flag: The cause is likely the cache.
|
|
@@ -58,6 +65,7 @@ from hooks_constants.reply_length_gate_constants import (
|
|
|
58
65
|
ALL_DECISION_CARD_PROSE_KEYS,
|
|
59
66
|
ALL_DECISION_OPTION_PROSE_KEYS,
|
|
60
67
|
ALL_DEFAULT_BANNED_WORDS,
|
|
68
|
+
ALL_EVIDENCE_PATTERNS,
|
|
61
69
|
ALLOW_EXIT_CODE,
|
|
62
70
|
BANNED_WORD_MESSAGE,
|
|
63
71
|
BANNED_WORD_PART_SEPARATOR,
|
|
@@ -66,6 +74,7 @@ from hooks_constants.reply_length_gate_constants import (
|
|
|
66
74
|
BANNED_WORDS_JSON_KEY,
|
|
67
75
|
BANNED_WORDS_PATH_ENV_VAR,
|
|
68
76
|
BLOCK_EXIT_CODE,
|
|
77
|
+
CAUSAL_CLAIM_PATTERN,
|
|
69
78
|
CARD_TEXT_SEPARATOR,
|
|
70
79
|
CLAUDE_HOME_DIRECTORY_NAME,
|
|
71
80
|
CONFIG_FILE_ENCODING,
|
|
@@ -94,6 +103,7 @@ from hooks_constants.reply_length_gate_constants import (
|
|
|
94
103
|
TOOL_NAME_KEY,
|
|
95
104
|
UNLINKED_PULL_REQUEST_MESSAGE,
|
|
96
105
|
UNLINKED_PULL_REQUEST_PATTERN,
|
|
106
|
+
UNSOURCED_CAUSE_MESSAGE,
|
|
97
107
|
URL_PATTERN,
|
|
98
108
|
WORD_PATTERN,
|
|
99
109
|
WORD_SEPARATOR,
|
|
@@ -151,6 +161,21 @@ def em_dash_violation(reply_text: str) -> str | None:
|
|
|
151
161
|
return EM_DASH_MESSAGE
|
|
152
162
|
|
|
153
163
|
|
|
164
|
+
def unsourced_cause_violation(reply_text: str) -> str | None:
|
|
165
|
+
"""Return the deny reason for the first causal sentence that cites no evidence itself, or None."""
|
|
166
|
+
all_sentences = [
|
|
167
|
+
each_sentence
|
|
168
|
+
for each_line in LINE_BREAK_PATTERN.split(FENCED_BLOCK_PATTERN.sub(" ", reply_text))
|
|
169
|
+
for each_sentence in SENTENCE_END_PATTERN.split(each_line)
|
|
170
|
+
]
|
|
171
|
+
for each_sentence in all_sentences:
|
|
172
|
+
is_causal = CAUSAL_CLAIM_PATTERN.search(INLINE_CODE_PATTERN.sub(" ", each_sentence))
|
|
173
|
+
has_evidence = any(each_pattern.search(each_sentence) for each_pattern in ALL_EVIDENCE_PATTERNS)
|
|
174
|
+
if is_causal and not has_evidence:
|
|
175
|
+
return UNSOURCED_CAUSE_MESSAGE.format(sentence=each_sentence.strip())
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
|
|
154
179
|
def unlinked_pull_request_violation(reply_text: str) -> str | None:
|
|
155
180
|
"""Return the deny reason for a pull request number outside a link, or None."""
|
|
156
181
|
without_fences = FENCED_BLOCK_PATTERN.sub(" ", reply_text)
|
|
@@ -231,6 +256,7 @@ def tool_violation(tool_name: object, all_tool_input: dict[str, object]) -> tupl
|
|
|
231
256
|
or mid_sentence_colon_violation(reply_text)
|
|
232
257
|
or em_dash_violation(reply_text)
|
|
233
258
|
or unlinked_pull_request_violation(reply_text)
|
|
259
|
+
or unsourced_cause_violation(reply_text)
|
|
234
260
|
or banned_word_violation(reply_text, configured_banned_words())
|
|
235
261
|
)
|
|
236
262
|
return None if violation is None else (violation, reply_text)
|
|
@@ -487,3 +487,55 @@ def test_should_deny_an_em_dash_and_allow_one_in_code(
|
|
|
487
487
|
)
|
|
488
488
|
assert (denied_code, allowed_code) == (2, 0)
|
|
489
489
|
assert "em dash" in denied_text
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def test_should_deny_the_quoted_cause_with_no_source(
|
|
493
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
494
|
+
) -> None:
|
|
495
|
+
exit_code, stderr_text = run_gate(
|
|
496
|
+
monkeypatch,
|
|
497
|
+
capsys,
|
|
498
|
+
POST_TOOL_NAME,
|
|
499
|
+
{"text": "That window was Windows PowerShell without Administrator, so the write did not land."},
|
|
500
|
+
)
|
|
501
|
+
assert exit_code == 2
|
|
502
|
+
assert "Cause with no source" in stderr_text
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
@pytest.mark.parametrize(
|
|
506
|
+
"reply_text",
|
|
507
|
+
[
|
|
508
|
+
"The write did not land, because the next read printed `Right after write: 1`.",
|
|
509
|
+
"The job failed because of [run 12](https://github.com/owner/repo/actions/runs/12).",
|
|
510
|
+
],
|
|
511
|
+
)
|
|
512
|
+
def test_should_allow_a_cause_that_cites_evidence(
|
|
513
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], reply_text: str
|
|
514
|
+
) -> None:
|
|
515
|
+
exit_code, stderr_text = run_gate(monkeypatch, capsys, REPLY_TOOL_NAME, {"text": reply_text})
|
|
516
|
+
assert (exit_code, stderr_text) == (0, "")
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
@pytest.mark.parametrize(
|
|
520
|
+
"reply_text",
|
|
521
|
+
[
|
|
522
|
+
"That window lacked Administrator, so the write did not land.\n\n```powershell\nRestart-Service x\n```",
|
|
523
|
+
"The error shows the hook is the only line. So the filter wrote nothing.",
|
|
524
|
+
"One line survived because its start did not match my filter. Run this.",
|
|
525
|
+
],
|
|
526
|
+
)
|
|
527
|
+
def test_should_deny_a_cause_whose_own_sentence_cites_nothing(
|
|
528
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], reply_text: str
|
|
529
|
+
) -> None:
|
|
530
|
+
exit_code, stderr_text = run_gate(monkeypatch, capsys, REPLY_TOOL_NAME, {"text": reply_text})
|
|
531
|
+
assert exit_code == 2
|
|
532
|
+
assert "Cause with no source" in stderr_text
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def test_should_allow_a_reply_with_no_causal_claim(
|
|
536
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
537
|
+
) -> None:
|
|
538
|
+
exit_code, _ = run_gate(
|
|
539
|
+
monkeypatch, capsys, REPLY_TOOL_NAME, {"text": "The runner restarted. Also, so far so good."}
|
|
540
|
+
)
|
|
541
|
+
assert exit_code == 0
|
|
@@ -4,7 +4,7 @@ This family stops a chat reply, edited message, or tool call when its payload br
|
|
|
4
4
|
|
|
5
5
|
## Checks
|
|
6
6
|
|
|
7
|
-
- `blocking/reply_length_gate.py` caps chat reply length, denies a configured banned word, and requires a readable link when the text names a pull request number.
|
|
7
|
+
- `blocking/reply_length_gate.py` caps chat reply length, denies a configured banned word, and requires a readable link when the text names a pull request number, and denies a causal claim in a reply that cites no code span, output block, or link.
|
|
8
8
|
- `blocking/edit_marker_gate.py` rejects strikethrough and edit-note markers in replacement chat text or cards.
|
|
9
9
|
- `blocking/issue_close_handoff_gate.py` denies an issue comment or issue write that closes the issue and routes a found defect to another issue or epic.
|
|
10
10
|
- `blocking/session_title_format_gate.py` denies a session title that breaks the `<emoji> <name>` shape or has a name longer than 25 characters.
|
|
@@ -40,6 +40,15 @@ MID_SENTENCE_COLON_MESSAGE = (
|
|
|
40
40
|
" and put the list on the lines below."
|
|
41
41
|
)
|
|
42
42
|
EM_DASH_MESSAGE = "An em dash is in the text. Use a period or a comma."
|
|
43
|
+
CAUSAL_CLAIM_PATTERN = re.compile(
|
|
44
|
+
r"(?:^|,)\s*so\b(?!\s+far\b)|\b(?:because|caused|causes|due to|therefore|which means|that means)\b",
|
|
45
|
+
re.IGNORECASE,
|
|
46
|
+
)
|
|
47
|
+
ALL_EVIDENCE_PATTERNS = (INLINE_CODE_PATTERN, URL_PATTERN, MARKDOWN_LINK_PATTERN)
|
|
48
|
+
UNSOURCED_CAUSE_MESSAGE = (
|
|
49
|
+
'Cause with no source: "{sentence}". Quote the log line or output in backticks,'
|
|
50
|
+
" or link the run or file and line that shows it."
|
|
51
|
+
)
|
|
43
52
|
ALL_DEFAULT_BANNED_WORDS = (
|
|
44
53
|
"real",
|
|
45
54
|
"really",
|