claude-dev-env 8.53.1 → 8.53.2

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.
@@ -10,6 +10,13 @@ It also denies a pull request number the reader cannot open::
10
10
  flag: It merged in #4347.
11
11
  ok: Both land in [PR 5256](https://github.com/owner/repo/pull/5256).
12
12
 
13
+ It denies a colon that joins two clauses on one line, and an em dash::
14
+
15
+ flag: That second reading matters: one message added 6.7k.
16
+ ok: That second reading matters. One message added 6.7k.
17
+ ok: Two checks failed at 9:47, then the list follows:
18
+ (each item on its own line below the colon)
19
+
13
20
  It denies a banned word or phrase, matched whole and case-insensitive::
14
21
 
15
22
  flag: The cause is likely the cache.
@@ -64,6 +71,8 @@ from hooks_constants.reply_length_gate_constants import (
64
71
  CONFIG_FILE_ENCODING,
65
72
  DECISION_CARD_OPTIONS_KEY,
66
73
  DECISION_CARD_TOOL_NAME,
74
+ EM_DASH_MESSAGE,
75
+ EM_DASH_PATTERN,
67
76
  FENCED_BLOCK_PATTERN,
68
77
  HOOK_EVENT_NAME,
69
78
  INLINE_CODE_PATTERN,
@@ -73,6 +82,8 @@ from hooks_constants.reply_length_gate_constants import (
73
82
  MARKDOWN_LINK_PATTERN,
74
83
  MAXIMUM_SENTENCE_COUNT,
75
84
  MAXIMUM_WORDS_PER_SENTENCE,
85
+ MID_SENTENCE_COLON_MESSAGE,
86
+ MID_SENTENCE_COLON_PATTERN,
76
87
  RETRY_INSTRUCTION,
77
88
  SENTENCE_END_PATTERN,
78
89
  SENTENCE_PREVIEW_SUFFIX,
@@ -126,6 +137,20 @@ def length_violation(reply_text: str) -> str | None:
126
137
  return None
127
138
 
128
139
 
140
+ def mid_sentence_colon_violation(reply_text: str) -> str | None:
141
+ """Return the deny reason for a colon followed by prose on its line, or None."""
142
+ if MID_SENTENCE_COLON_PATTERN.search(countable_text(reply_text)) is None:
143
+ return None
144
+ return MID_SENTENCE_COLON_MESSAGE
145
+
146
+
147
+ def em_dash_violation(reply_text: str) -> str | None:
148
+ """Return the deny reason for an em dash in the prose, or None."""
149
+ if EM_DASH_PATTERN.search(countable_text(reply_text)) is None:
150
+ return None
151
+ return EM_DASH_MESSAGE
152
+
153
+
129
154
  def unlinked_pull_request_violation(reply_text: str) -> str | None:
130
155
  """Return the deny reason for a pull request number outside a link, or None."""
131
156
  without_fences = FENCED_BLOCK_PATTERN.sub(" ", reply_text)
@@ -203,6 +228,8 @@ def tool_violation(tool_name: object, all_tool_input: dict[str, object]) -> tupl
203
228
  return None
204
229
  violation = (
205
230
  length_violation(reply_text)
231
+ or mid_sentence_colon_violation(reply_text)
232
+ or em_dash_violation(reply_text)
206
233
  or unlinked_pull_request_violation(reply_text)
207
234
  or banned_word_violation(reply_text, configured_banned_words())
208
235
  )
@@ -442,3 +442,48 @@ def test_should_apply_the_configured_list_to_decision_cards(
442
442
  )
443
443
  assert (probably_exit_code, synergy_exit_code) == (0, 2)
444
444
  assert 'Banned word "synergy"' in stderr_text
445
+
446
+
447
+ QUOTED_COLON_SENTENCE = "That second reading matters: one message added 6.7k to Messages."
448
+
449
+
450
+ def test_should_deny_the_quoted_mid_sentence_colon(
451
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
452
+ ) -> None:
453
+ exit_code, stderr_text = run_gate(
454
+ monkeypatch, capsys, POST_TOOL_NAME, {"text": QUOTED_COLON_SENTENCE}
455
+ )
456
+ assert exit_code == 2
457
+ assert "colon" in stderr_text
458
+
459
+
460
+ @pytest.mark.parametrize(
461
+ "allowed_text",
462
+ [
463
+ "That second reading matters. One message added 6.7k to Messages.",
464
+ "Two checks failed:\n- lint\n- types",
465
+ "It fired at 9:47 today.",
466
+ "The call `a: int` passed.",
467
+ "See https://example.com/a: b for it.",
468
+ "Both land in [PR 5256](https://github.com/owner/repo/pull/5256).",
469
+ "```\nflag: x\n```",
470
+ ],
471
+ )
472
+ def test_should_allow_a_colon_that_introduces_a_list_or_sits_in_a_span(
473
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], allowed_text: str
474
+ ) -> None:
475
+ exit_code, stderr_text = run_gate(monkeypatch, capsys, REPLY_TOOL_NAME, {"text": allowed_text})
476
+ assert (exit_code, stderr_text) == (0, "")
477
+
478
+
479
+ def test_should_deny_an_em_dash_and_allow_one_in_code(
480
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
481
+ ) -> None:
482
+ denied_code, denied_text = run_gate(
483
+ monkeypatch, capsys, REPLY_TOOL_NAME, {"text": "The fix \u2014 a rename \u2014 is in."}
484
+ )
485
+ allowed_code, _ = run_gate(
486
+ monkeypatch, capsys, REPLY_TOOL_NAME, {"text": "The fix uses `\u2014` in a label."}
487
+ )
488
+ assert (denied_code, allowed_code) == (2, 0)
489
+ assert "em dash" in denied_text
@@ -24,6 +24,8 @@ MARKDOWN_LINK_PATTERN = re.compile(r"\[[^\]\n]*\]\([^)\s]*\)")
24
24
  UNLINKED_PULL_REQUEST_PATTERN = re.compile(
25
25
  r"\b(?:PRs?|pull requests?)\s*#?\d+|(?<![\w/&])#\d+", re.IGNORECASE
26
26
  )
27
+ MID_SENTENCE_COLON_PATTERN = re.compile(r":[ \t]+\S")
28
+ EM_DASH_PATTERN = re.compile("\u2014")
27
29
  LINE_BREAK_PATTERN = re.compile(r"\n+")
28
30
  SENTENCE_END_PATTERN = re.compile(r"(?<=[.!?])\s+")
29
31
  WORD_PATTERN = re.compile(r"[A-Za-z0-9]+(?:['.,-][A-Za-z0-9]+)*")
@@ -33,6 +35,11 @@ SENTENCE_PREVIEW_SUFFIX = "..."
33
35
  RETRY_INSTRUCTION = " Cut the text and resend the call."
34
36
  TOO_MANY_SENTENCES_MESSAGE = "Reply too long: {sentence_count} sentences, limit {sentence_limit}."
35
37
  UNLINKED_PULL_REQUEST_MESSAGE = 'Pull request "{reference}" has no link. Write it as [PR N](https://github.com/<owner>/<repo>/pull/N).'
38
+ MID_SENTENCE_COLON_MESSAGE = (
39
+ "A colon joins two clauses on one line. Write two sentences, or end the line with the colon"
40
+ " and put the list on the lines below."
41
+ )
42
+ EM_DASH_MESSAGE = "An em dash is in the text. Use a period or a comma."
36
43
  ALL_DEFAULT_BANNED_WORDS = (
37
44
  "real",
38
45
  "really",
@@ -45,3 +45,9 @@ def test_gate_reads_only_the_prose_fields_of_a_decision_card() -> None:
45
45
  constants.DECISION_CARD_OPTIONS_KEY,
46
46
  constants.ALL_DECISION_OPTION_PROSE_KEYS,
47
47
  ) == (("question", "context"), "options", ("label", "consequence"))
48
+
49
+
50
+ def test_colon_pattern_matches_a_colon_with_prose_after_it_on_the_line() -> None:
51
+ assert constants.MID_SENTENCE_COLON_PATTERN.search("matters: one message")
52
+ assert constants.MID_SENTENCE_COLON_PATTERN.search("at 9:47") is None
53
+ assert constants.MID_SENTENCE_COLON_PATTERN.search("items:\n- one") is None
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.53.1",
3
+ "version": "8.53.2",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {