claude-dev-env 8.24.0 → 8.25.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.
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
The gate reads the ``text`` field of the chat tools that post to the user.
|
|
5
5
|
It denies the call when the text holds more than MAXIMUM_SENTENCE_COUNT
|
|
6
6
|
sentences, or a sentence longer than MAXIMUM_WORDS_PER_SENTENCE words.
|
|
7
|
+
It also denies a pull request number the reader cannot open::
|
|
8
|
+
|
|
9
|
+
flag: Both land in PR 5256.
|
|
10
|
+
flag: It merged in #4347.
|
|
11
|
+
ok: Both land in [PR 5256](https://github.com/owner/repo/pull/5256).
|
|
7
12
|
Each non-empty line counts as its own sentence, so a list counts one
|
|
8
13
|
sentence per item. URLs, markdown link targets, inline code spans, and
|
|
9
14
|
fenced blocks carry no words.
|
|
@@ -34,6 +39,7 @@ from hooks_constants.reply_length_gate_constants import (
|
|
|
34
39
|
LINE_BREAK_PATTERN,
|
|
35
40
|
LINK_TARGET_PATTERN,
|
|
36
41
|
LONG_SENTENCE_MESSAGE,
|
|
42
|
+
MARKDOWN_LINK_PATTERN,
|
|
37
43
|
MAXIMUM_SENTENCE_COUNT,
|
|
38
44
|
MAXIMUM_WORDS_PER_SENTENCE,
|
|
39
45
|
RETRY_INSTRUCTION,
|
|
@@ -44,6 +50,8 @@ from hooks_constants.reply_length_gate_constants import (
|
|
|
44
50
|
TOO_MANY_SENTENCES_MESSAGE,
|
|
45
51
|
TOOL_INPUT_KEY,
|
|
46
52
|
TOOL_NAME_KEY,
|
|
53
|
+
UNLINKED_PULL_REQUEST_MESSAGE,
|
|
54
|
+
UNLINKED_PULL_REQUEST_PATTERN,
|
|
47
55
|
URL_PATTERN,
|
|
48
56
|
WORD_PATTERN,
|
|
49
57
|
WORD_SEPARATOR,
|
|
@@ -87,6 +95,17 @@ def length_violation(reply_text: str) -> str | None:
|
|
|
87
95
|
return None
|
|
88
96
|
|
|
89
97
|
|
|
98
|
+
def unlinked_pull_request_violation(reply_text: str) -> str | None:
|
|
99
|
+
"""Return the deny reason for a pull request number outside a link, or None."""
|
|
100
|
+
without_fences = FENCED_BLOCK_PATTERN.sub(" ", reply_text)
|
|
101
|
+
without_code = INLINE_CODE_PATTERN.sub(" ", without_fences)
|
|
102
|
+
without_links = MARKDOWN_LINK_PATTERN.sub(" ", without_code)
|
|
103
|
+
unlinked_match = UNLINKED_PULL_REQUEST_PATTERN.search(URL_PATTERN.sub(" ", without_links))
|
|
104
|
+
if unlinked_match is None:
|
|
105
|
+
return None
|
|
106
|
+
return UNLINKED_PULL_REQUEST_MESSAGE.format(reference=unlinked_match.group(0))
|
|
107
|
+
|
|
108
|
+
|
|
90
109
|
def main() -> int:
|
|
91
110
|
hook_input = read_hook_input_dictionary_from_stdin()
|
|
92
111
|
if hook_input is None:
|
|
@@ -100,7 +119,7 @@ def main() -> int:
|
|
|
100
119
|
reply_text = tool_input.get(TEXT_KEY)
|
|
101
120
|
if not isinstance(reply_text, str):
|
|
102
121
|
return ALLOW_EXIT_CODE
|
|
103
|
-
violation = length_violation(reply_text)
|
|
122
|
+
violation = length_violation(reply_text) or unlinked_pull_request_violation(reply_text)
|
|
104
123
|
if violation is None:
|
|
105
124
|
return ALLOW_EXIT_CODE
|
|
106
125
|
block_reason = violation + RETRY_INSTRUCTION
|
|
@@ -191,3 +191,47 @@ def test_should_allow_a_call_without_text(
|
|
|
191
191
|
def test_should_allow_empty_stdin(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
192
192
|
monkeypatch.setattr("sys.stdin", io.TextIOWrapper(io.BytesIO(b"")))
|
|
193
193
|
assert reply_length_gate.main() == 0
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_should_deny_a_pr_number_with_no_link_and_name_it(
|
|
197
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
198
|
+
) -> None:
|
|
199
|
+
exit_code, stderr_text = run_gate(
|
|
200
|
+
monkeypatch, capsys, REPLY_TOOL_NAME, {"text": "Both land in PR 5256."}
|
|
201
|
+
)
|
|
202
|
+
assert exit_code == 2
|
|
203
|
+
assert "PR 5256" in stderr_text
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_should_deny_a_bare_hash_number(
|
|
207
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
208
|
+
) -> None:
|
|
209
|
+
exit_code, stderr_text = run_gate(
|
|
210
|
+
monkeypatch, capsys, POST_TOOL_NAME, {"text": "It merged in #4347."}
|
|
211
|
+
)
|
|
212
|
+
assert exit_code == 2
|
|
213
|
+
assert "#4347" in stderr_text
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_should_allow_a_pr_number_inside_its_link(
|
|
217
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
218
|
+
) -> None:
|
|
219
|
+
exit_code, stderr_text = run_gate(
|
|
220
|
+
monkeypatch,
|
|
221
|
+
capsys,
|
|
222
|
+
REPLY_TOOL_NAME,
|
|
223
|
+
{"text": "Both land in [PR 5256](https://github.com/owner/repo/pull/5256)."},
|
|
224
|
+
)
|
|
225
|
+
assert (exit_code, stderr_text) == (0, "")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_should_allow_a_bare_pull_request_url(
|
|
229
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
230
|
+
) -> None:
|
|
231
|
+
exit_code, _ = run_gate(
|
|
232
|
+
monkeypatch,
|
|
233
|
+
capsys,
|
|
234
|
+
REPLY_TOOL_NAME,
|
|
235
|
+
{"text": "Pull request: https://github.com/owner/repo/pull/5256"},
|
|
236
|
+
)
|
|
237
|
+
assert exit_code == 0
|
|
@@ -15,6 +15,10 @@ FENCED_BLOCK_PATTERN = re.compile(r"```.*?(?:```|\Z)", re.DOTALL)
|
|
|
15
15
|
INLINE_CODE_PATTERN = re.compile(r"`[^`\n]*`")
|
|
16
16
|
LINK_TARGET_PATTERN = re.compile(r"\]\([^)\s]*\)")
|
|
17
17
|
URL_PATTERN = re.compile(r"(?:https?://|www\.)\S+")
|
|
18
|
+
MARKDOWN_LINK_PATTERN = re.compile(r"\[[^\]\n]*\]\([^)\s]*\)")
|
|
19
|
+
UNLINKED_PULL_REQUEST_PATTERN = re.compile(
|
|
20
|
+
r"\b(?:PRs?|pull requests?)\s*#?\d+|(?<![\w/&])#\d+", re.IGNORECASE
|
|
21
|
+
)
|
|
18
22
|
LINE_BREAK_PATTERN = re.compile(r"\n+")
|
|
19
23
|
SENTENCE_END_PATTERN = re.compile(r"(?<=[.!?])\s+")
|
|
20
24
|
WORD_PATTERN = re.compile(r"[A-Za-z0-9]+(?:['.,-][A-Za-z0-9]+)*")
|
|
@@ -23,6 +27,7 @@ WORD_SEPARATOR = " "
|
|
|
23
27
|
SENTENCE_PREVIEW_SUFFIX = "..."
|
|
24
28
|
RETRY_INSTRUCTION = " Cut the text and resend the call."
|
|
25
29
|
TOO_MANY_SENTENCES_MESSAGE = "Reply too long: {sentence_count} sentences, limit {sentence_limit}."
|
|
30
|
+
UNLINKED_PULL_REQUEST_MESSAGE = 'Pull request "{reference}" has no link. Write it as [PR N](https://github.com/<owner>/<repo>/pull/N).'
|
|
26
31
|
LONG_SENTENCE_MESSAGE = (
|
|
27
32
|
'Sentence too long: {word_count} words, limit {word_limit}: "{sentence_preview}".'
|
|
28
33
|
)
|