claude-dev-env 8.53.1 → 8.53.3
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/hooks/blocking/reply_length_gate.py +27 -0
- package/hooks/blocking/test_reply_length_gate.py +45 -0
- package/hooks/hooks_constants/reply_length_gate_constants.py +7 -0
- package/hooks/hooks_constants/test_reply_length_gate_constants.py +6 -0
- package/package.json +1 -1
- package/scripts/account_broker.py +4 -2
- package/scripts/dev_env_scripts_constants/codex_account_constants.py +3 -0
- package/scripts/test_account_broker.py +32 -0
|
@@ -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
|
@@ -54,6 +54,7 @@ from dev_env_scripts_constants.account_broker_constants import (
|
|
|
54
54
|
from dev_env_scripts_constants.codex_account_constants import (
|
|
55
55
|
CODEX_HOME_ENVIRONMENT_VARIABLE,
|
|
56
56
|
MAIN_CODEX_HOME_DIRECTORY_NAME,
|
|
57
|
+
NO_ROSTER_ACCOUNT_NAME,
|
|
57
58
|
TIER_NORMAL,
|
|
58
59
|
)
|
|
59
60
|
|
|
@@ -78,7 +79,7 @@ def choose_from_readings(
|
|
|
78
79
|
"""
|
|
79
80
|
if product is Product.CODEX and not all_readings:
|
|
80
81
|
home = Path(os.environ.get(CODEX_HOME_ENVIRONMENT_VARIABLE) or Path.home() / MAIN_CODEX_HOME_DIRECTORY_NAME).resolve()
|
|
81
|
-
return Decision("run", Account(product,
|
|
82
|
+
return Decision("run", Account(product, NO_ROSTER_ACCOUNT_NAME, home, True), None, "no roster is configured", TIER_NORMAL)
|
|
82
83
|
all_available = [each_reading for each_reading in all_readings if each_reading.account not in all_spent_accounts]
|
|
83
84
|
selected = (
|
|
84
85
|
_choose_claude(all_available, preferred_command)
|
|
@@ -366,7 +367,8 @@ def _parse_spent_mark(all_accounts_by_name: Mapping[str, Reading], mark: str, pr
|
|
|
366
367
|
raise BrokerConfigurationError(f"invalid reset for account {name}") from error
|
|
367
368
|
reading = all_accounts_by_name.get(name)
|
|
368
369
|
if reading is None:
|
|
369
|
-
|
|
370
|
+
if all_accounts_by_name or name != NO_ROSTER_ACCOUNT_NAME:
|
|
371
|
+
print(f"warning: spent mark names {name}, an account outside the roster; choose waits until its reset", file=sys.stderr)
|
|
370
372
|
return Reading(Account(product, name, Path(), False), None), reset
|
|
371
373
|
return reading, reset
|
|
372
374
|
|
|
@@ -66,6 +66,9 @@ CODEX_PROFILES_ROOT_ENVIRONMENT_VARIABLE: str = "CODEX_PROFILES_ROOT"
|
|
|
66
66
|
MAIN_CODEX_HOME_DIRECTORY_NAME: str = ".codex"
|
|
67
67
|
"""Directory under the user home that holds the shared Codex setup."""
|
|
68
68
|
|
|
69
|
+
NO_ROSTER_ACCOUNT_NAME: str = "default"
|
|
70
|
+
"""Account name the broker gives the shared Codex home when no roster is configured."""
|
|
71
|
+
|
|
69
72
|
CODEX_HOME_ENVIRONMENT_VARIABLE: str = "CODEX_HOME"
|
|
70
73
|
"""Environment variable that points Codex at one account's home."""
|
|
71
74
|
|
|
@@ -1026,6 +1026,38 @@ def test_should_warn_when_a_spent_mark_names_an_account_outside_the_roster(
|
|
|
1026
1026
|
assert "outside the roster" in all_warning_lines[0]
|
|
1027
1027
|
|
|
1028
1028
|
|
|
1029
|
+
def test_should_not_warn_when_the_default_account_is_marked_without_a_roster(
|
|
1030
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
1031
|
+
) -> None:
|
|
1032
|
+
monkeypatch.setitem(account_broker.all_product_adapters, Product.CODEX, _adapter((), {}))
|
|
1033
|
+
_freeze_clock(monkeypatch, NOW)
|
|
1034
|
+
reset = NOW + timedelta(hours=2)
|
|
1035
|
+
|
|
1036
|
+
account_broker.main(("choose", "--product", "codex", "--spent", f"default:{int(reset.timestamp())}"))
|
|
1037
|
+
|
|
1038
|
+
assert capsys.readouterr().err == ""
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
@pytest.mark.parametrize(
|
|
1042
|
+
("all_roster_names", "marked_name"),
|
|
1043
|
+
[(("first",), "default"), ((), "visitor")],
|
|
1044
|
+
)
|
|
1045
|
+
def test_should_warn_when_a_spent_mark_names_an_account_the_roster_lacks(
|
|
1046
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], all_roster_names: tuple[str, ...], marked_name: str
|
|
1047
|
+
) -> None:
|
|
1048
|
+
all_accounts = tuple(_account(each_name) for each_name in all_roster_names)
|
|
1049
|
+
monkeypatch.setitem(account_broker.all_product_adapters, Product.CODEX, _adapter(all_accounts, {each_name: _meters(80, 80) for each_name in all_roster_names}))
|
|
1050
|
+
_freeze_clock(monkeypatch, NOW)
|
|
1051
|
+
reset = NOW + timedelta(hours=2)
|
|
1052
|
+
|
|
1053
|
+
account_broker.main(("choose", "--product", "codex", "--spent", f"{marked_name}:{int(reset.timestamp())}"))
|
|
1054
|
+
|
|
1055
|
+
all_warning_lines = capsys.readouterr().err.splitlines()
|
|
1056
|
+
assert len(all_warning_lines) == 1
|
|
1057
|
+
assert marked_name in all_warning_lines[0]
|
|
1058
|
+
assert "outside the roster" in all_warning_lines[0]
|
|
1059
|
+
|
|
1060
|
+
|
|
1029
1061
|
@pytest.mark.parametrize("mark", ["", ":1800000000"])
|
|
1030
1062
|
def test_should_reject_a_spent_mark_without_an_account_name(
|
|
1031
1063
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], mark: str
|