claude-dev-env 8.105.1 → 8.106.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/docs/rule-guides/filesystem-search.md +3 -2
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +5 -1
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +1 -0
- package/hooks/blocking/test_transcript_root_powershell_starts.py +79 -0
- package/hooks/blocking/test_transcript_root_search_gate.py +180 -0
- package/hooks/blocking/test_transcript_root_search_starts.py +85 -0
- package/hooks/blocking/test_transcript_root_shell_starts.py +131 -0
- package/hooks/blocking/transcript_root_powershell_starts.py +114 -0
- package/hooks/blocking/transcript_root_search_gate.py +120 -0
- package/hooks/blocking/transcript_root_search_starts.py +168 -0
- package/hooks/blocking/transcript_root_shell_starts.py +368 -0
- package/hooks/features/bash-dispatchers.md +3 -0
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -1
- package/hooks/hooks_constants/transcript_root_search_gate_constants.py +301 -0
- package/package.json +1 -1
- package/scripts/dev_env_scripts_constants/account_broker_constants.py +22 -4
- package/scripts/policy_lint/config/constants.py +1 -0
- package/scripts/test_account_broker.py +20 -6
package/hooks/hooks.json
CHANGED
|
@@ -152,6 +152,16 @@
|
|
|
152
152
|
}
|
|
153
153
|
]
|
|
154
154
|
},
|
|
155
|
+
{
|
|
156
|
+
"matcher": "Grep|Glob",
|
|
157
|
+
"hooks": [
|
|
158
|
+
{
|
|
159
|
+
"type": "command",
|
|
160
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/transcript_root_search_gate.py",
|
|
161
|
+
"timeout": 10
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
},
|
|
155
165
|
{
|
|
156
166
|
"matcher": "EnterWorktree",
|
|
157
167
|
"hooks": [
|
|
@@ -83,4 +83,8 @@ ALL_BASH_HOSTED_HOOK_ENTRIES: tuple[BashHostedHookEntry, ...] = (
|
|
|
83
83
|
script_relative_path="blocking/python_windows_invocation_gate.py",
|
|
84
84
|
applicable_tool_names=ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
|
|
85
85
|
),
|
|
86
|
+
BashHostedHookEntry(
|
|
87
|
+
script_relative_path="blocking/transcript_root_search_gate.py",
|
|
88
|
+
applicable_tool_names=ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
|
|
89
|
+
),
|
|
86
90
|
)
|
|
@@ -12,7 +12,7 @@ ALL_BASH_AND_POWERSHELL_TOOL_NAMES = ALL_CONSTANT_BINDINGS["ALL_BASH_AND_POWERSH
|
|
|
12
12
|
BashHostedHookEntry = ALL_CONSTANT_BINDINGS["BashHostedHookEntry"]
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def
|
|
15
|
+
def test_roster_hosts_the_six_gates_and_hands_two_to_the_shell_guards_mod() -> None:
|
|
16
16
|
assert ALL_BASH_HOSTED_HOOK_ENTRIES == (
|
|
17
17
|
BashHostedHookEntry(
|
|
18
18
|
script_relative_path="blocking/msys_rev_path_rewriter.py",
|
|
@@ -36,4 +36,8 @@ def test_roster_hosts_the_five_gates_and_hands_two_to_the_shell_guards_mod() ->
|
|
|
36
36
|
script_relative_path="blocking/python_windows_invocation_gate.py",
|
|
37
37
|
applicable_tool_names=ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
|
|
38
38
|
),
|
|
39
|
+
BashHostedHookEntry(
|
|
40
|
+
script_relative_path="blocking/transcript_root_search_gate.py",
|
|
41
|
+
applicable_tool_names=ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
|
|
42
|
+
),
|
|
39
43
|
)
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Constants for the gate that stops walks and content searches over a Claude transcript root.
|
|
2
|
+
|
|
3
|
+
Holds the transcript root pattern and the marker word every root path holds,
|
|
4
|
+
the path parts the gate joins and splits on, the shell assignment and variable
|
|
5
|
+
reference patterns, the directory-change programs, the option grammar of each
|
|
6
|
+
POSIX search program and the PowerShell switches, the Grep and Glob tool names
|
|
7
|
+
and input keys, the hook name the block log records, and the deny message that
|
|
8
|
+
names the list-then-read fix.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"TRANSCRIPT_ROOT_PATTERN",
|
|
18
|
+
"TRANSCRIPT_ROOT_MARKER",
|
|
19
|
+
"PATH_SEPARATOR",
|
|
20
|
+
"WINDOWS_PATH_SEPARATOR",
|
|
21
|
+
"CURRENT_DIRECTORY",
|
|
22
|
+
"POWERSHELL_PATH_LIST_SEPARATOR",
|
|
23
|
+
"WILDCARD_ONLY_SEGMENT_PATTERN",
|
|
24
|
+
"RECURSIVE_GLOB_SEGMENT",
|
|
25
|
+
"ABSOLUTE_PATH_PATTERN",
|
|
26
|
+
"SHELL_ASSIGNMENT_PATTERN",
|
|
27
|
+
"SPACED_ASSIGNMENT_NAME_PATTERN",
|
|
28
|
+
"SPACED_ASSIGNMENT_OPERATOR",
|
|
29
|
+
"SPACED_ASSIGNMENT_TOKEN_COUNT",
|
|
30
|
+
"ALL_ASSIGNMENT_PREFIX_WORDS",
|
|
31
|
+
"SHELL_VARIABLE_SIGIL",
|
|
32
|
+
"SHELL_VARIABLE_REFERENCE_TEMPLATE",
|
|
33
|
+
"ALL_CHANGE_DIRECTORY_PROGRAM_NAMES",
|
|
34
|
+
"ALL_HOME_DEFAULT_CHANGE_DIRECTORY_PROGRAM_NAMES",
|
|
35
|
+
"ALL_PUSH_DIRECTORY_PROGRAM_NAMES",
|
|
36
|
+
"ALL_POP_DIRECTORY_PROGRAM_NAMES",
|
|
37
|
+
"HOME_DIRECTORY",
|
|
38
|
+
"FIND_MAXIMUM_DEPTH_OPTION",
|
|
39
|
+
"ALL_SHALLOW_DEPTH_VALUES",
|
|
40
|
+
"OPTION_PREFIX",
|
|
41
|
+
"LONG_OPTION_PREFIX",
|
|
42
|
+
"END_OF_OPTIONS_TOKEN",
|
|
43
|
+
"GLUED_OPTION_VALUE_SEPARATOR",
|
|
44
|
+
"RECURSE_DIRECTORIES_ACTION",
|
|
45
|
+
"PosixSearchGrammar",
|
|
46
|
+
"ALL_GREP_PROGRAM_NAMES",
|
|
47
|
+
"GREP_GRAMMAR",
|
|
48
|
+
"ALL_INDEXED_SEARCH_PROGRAM_NAMES",
|
|
49
|
+
"INDEXED_SEARCH_GRAMMAR",
|
|
50
|
+
"UNIX_LS_GRAMMAR",
|
|
51
|
+
"TREE_PROGRAM_NAME",
|
|
52
|
+
"TREE_GRAMMAR",
|
|
53
|
+
"POWERSHELL_TOOL_NAME",
|
|
54
|
+
"POWERSHELL_DEPTH_OPTION",
|
|
55
|
+
"POWERSHELL_NON_RECURSIVE_DEPTH",
|
|
56
|
+
"ALL_POWERSHELL_SWITCH_OPTIONS",
|
|
57
|
+
"POWERSHELL_GLUED_VALUE_SEPARATOR",
|
|
58
|
+
"POWERSHELL_MINIMUM_ABBREVIATION_LENGTH",
|
|
59
|
+
"ALL_POWERSHELL_ABBREVIABLE_OPTIONS",
|
|
60
|
+
"ALL_SELECT_STRING_PROGRAM_NAMES",
|
|
61
|
+
"GLOB_TOOL_NAME",
|
|
62
|
+
"ALL_FILE_SEARCH_TOOL_NAMES",
|
|
63
|
+
"TOOL_INPUT_PATH_KEY",
|
|
64
|
+
"TOOL_INPUT_PATTERN_KEY",
|
|
65
|
+
"TOOL_INPUT_COMMAND_KEY",
|
|
66
|
+
"PAYLOAD_TOOL_NAME_KEY",
|
|
67
|
+
"PAYLOAD_TOOL_INPUT_KEY",
|
|
68
|
+
"PAYLOAD_CWD_KEY",
|
|
69
|
+
"GATE_HOOK_NAME",
|
|
70
|
+
"TRANSCRIPT_ROOT_DENY_REASON_TEMPLATE",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
TRANSCRIPT_ROOT_PATTERN = re.compile(
|
|
74
|
+
r"(?:^|/)(?:\.claude|\.claude-profiles/[^/]+|\.claude-[^/]+)/projects(?=/|$)",
|
|
75
|
+
re.IGNORECASE,
|
|
76
|
+
)
|
|
77
|
+
TRANSCRIPT_ROOT_MARKER = "projects"
|
|
78
|
+
PATH_SEPARATOR = "/"
|
|
79
|
+
WINDOWS_PATH_SEPARATOR = "\\"
|
|
80
|
+
CURRENT_DIRECTORY = "."
|
|
81
|
+
POWERSHELL_PATH_LIST_SEPARATOR = ","
|
|
82
|
+
WILDCARD_ONLY_SEGMENT_PATTERN = re.compile(r"[*?.]*\*[*?.]*")
|
|
83
|
+
RECURSIVE_GLOB_SEGMENT = "**"
|
|
84
|
+
ABSOLUTE_PATH_PATTERN = re.compile(r"^(?:[A-Za-z]:|/|\\|~|\$)")
|
|
85
|
+
|
|
86
|
+
SHELL_ASSIGNMENT_PATTERN = re.compile(
|
|
87
|
+
r"^\$?(?:env:)?(?P<name>[A-Za-z_][A-Za-z0-9_]*)=(?P<value>.+)$"
|
|
88
|
+
)
|
|
89
|
+
SPACED_ASSIGNMENT_NAME_PATTERN = re.compile(r"^\$(?:env:)?(?P<name>[A-Za-z_][A-Za-z0-9_]*)$")
|
|
90
|
+
SPACED_ASSIGNMENT_OPERATOR = "="
|
|
91
|
+
SPACED_ASSIGNMENT_TOKEN_COUNT = 3
|
|
92
|
+
ALL_ASSIGNMENT_PREFIX_WORDS: frozenset[str] = frozenset({"export", "local", "declare", "set"})
|
|
93
|
+
SHELL_VARIABLE_SIGIL = "$"
|
|
94
|
+
SHELL_VARIABLE_REFERENCE_TEMPLATE = r"\$(?:\{{{name}\}}|(?:env:)?{name}(?![A-Za-z0-9_]))"
|
|
95
|
+
ALL_CHANGE_DIRECTORY_PROGRAM_NAMES: frozenset[str] = frozenset(
|
|
96
|
+
{"cd", "chdir", "pushd", "set-location", "sl", "push-location"}
|
|
97
|
+
)
|
|
98
|
+
ALL_HOME_DEFAULT_CHANGE_DIRECTORY_PROGRAM_NAMES: frozenset[str] = frozenset(
|
|
99
|
+
{"cd", "chdir", "set-location", "sl"}
|
|
100
|
+
)
|
|
101
|
+
ALL_PUSH_DIRECTORY_PROGRAM_NAMES: frozenset[str] = frozenset({"pushd", "push-location"})
|
|
102
|
+
ALL_POP_DIRECTORY_PROGRAM_NAMES: frozenset[str] = frozenset({"popd", "pop-location"})
|
|
103
|
+
HOME_DIRECTORY = "~"
|
|
104
|
+
|
|
105
|
+
FIND_MAXIMUM_DEPTH_OPTION = "-maxdepth"
|
|
106
|
+
ALL_SHALLOW_DEPTH_VALUES: frozenset[str] = frozenset({"0", "1"})
|
|
107
|
+
OPTION_PREFIX = "-"
|
|
108
|
+
LONG_OPTION_PREFIX = "--"
|
|
109
|
+
END_OF_OPTIONS_TOKEN = "--"
|
|
110
|
+
GLUED_OPTION_VALUE_SEPARATOR = "="
|
|
111
|
+
RECURSE_DIRECTORIES_ACTION = "recurse"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True)
|
|
115
|
+
class PosixSearchGrammar:
|
|
116
|
+
"""How one POSIX search program reads its options and paths.
|
|
117
|
+
|
|
118
|
+
Attributes:
|
|
119
|
+
is_recursive_by_default: True when the program walks below its paths with no option.
|
|
120
|
+
all_recurse_options: Options that turn on recursion, such as ``-r`` or ``--recursive``.
|
|
121
|
+
all_value_letters: Short option letters that take a value, glued or as the next token.
|
|
122
|
+
all_value_options: Long options that take the next token as their value.
|
|
123
|
+
all_depth_options: Options whose value limits the depth; a shallow value stops recursion.
|
|
124
|
+
all_recurse_value_options: Options whose value ``recurse`` turns on recursion.
|
|
125
|
+
all_pattern_options: Options that supply the pattern or list files with no pattern,
|
|
126
|
+
so no positional pattern comes first.
|
|
127
|
+
takes_pattern_positional: True when the first positional is the pattern.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
is_recursive_by_default: bool
|
|
131
|
+
all_recurse_options: frozenset[str] = frozenset()
|
|
132
|
+
all_value_letters: frozenset[str] = frozenset()
|
|
133
|
+
all_value_options: frozenset[str] = frozenset()
|
|
134
|
+
all_depth_options: frozenset[str] = frozenset()
|
|
135
|
+
all_recurse_value_options: frozenset[str] = frozenset()
|
|
136
|
+
all_pattern_options: frozenset[str] = frozenset()
|
|
137
|
+
takes_pattern_positional: bool = False
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
ALL_GREP_PROGRAM_NAMES: frozenset[str] = frozenset({"grep", "egrep", "fgrep"})
|
|
141
|
+
GREP_GRAMMAR = PosixSearchGrammar(
|
|
142
|
+
is_recursive_by_default=False,
|
|
143
|
+
all_recurse_options=frozenset({"-r", "-R", "--recursive", "--dereference-recursive"}),
|
|
144
|
+
all_value_letters=frozenset({"e", "f", "m", "A", "B", "C", "d", "D"}),
|
|
145
|
+
all_value_options=frozenset(
|
|
146
|
+
{
|
|
147
|
+
"--regexp",
|
|
148
|
+
"--file",
|
|
149
|
+
"--max-count",
|
|
150
|
+
"--after-context",
|
|
151
|
+
"--before-context",
|
|
152
|
+
"--context",
|
|
153
|
+
"--directories",
|
|
154
|
+
"--devices",
|
|
155
|
+
"--include",
|
|
156
|
+
"--exclude",
|
|
157
|
+
"--exclude-dir",
|
|
158
|
+
"--exclude-from",
|
|
159
|
+
"--label",
|
|
160
|
+
"--group-separator",
|
|
161
|
+
}
|
|
162
|
+
),
|
|
163
|
+
all_recurse_value_options=frozenset({"-d", "--directories"}),
|
|
164
|
+
all_pattern_options=frozenset({"-e", "-f", "--regexp", "--file"}),
|
|
165
|
+
takes_pattern_positional=True,
|
|
166
|
+
)
|
|
167
|
+
ALL_INDEXED_SEARCH_PROGRAM_NAMES: frozenset[str] = frozenset({"rg", "ag", "ack", "fd", "fdfind"})
|
|
168
|
+
INDEXED_SEARCH_GRAMMAR = PosixSearchGrammar(
|
|
169
|
+
is_recursive_by_default=True,
|
|
170
|
+
all_value_letters=frozenset(
|
|
171
|
+
{"e", "f", "g", "t", "T", "m", "A", "B", "C", "d", "j", "M", "r", "E", "x", "X", "S", "G"}
|
|
172
|
+
),
|
|
173
|
+
all_value_options=frozenset(
|
|
174
|
+
{
|
|
175
|
+
"--regexp",
|
|
176
|
+
"--file",
|
|
177
|
+
"--glob",
|
|
178
|
+
"--iglob",
|
|
179
|
+
"--type",
|
|
180
|
+
"--type-not",
|
|
181
|
+
"--type-add",
|
|
182
|
+
"--max-count",
|
|
183
|
+
"--after-context",
|
|
184
|
+
"--before-context",
|
|
185
|
+
"--context",
|
|
186
|
+
"--max-depth",
|
|
187
|
+
"--maxdepth",
|
|
188
|
+
"--depth",
|
|
189
|
+
"--threads",
|
|
190
|
+
"--max-columns",
|
|
191
|
+
"--max-filesize",
|
|
192
|
+
"--sort",
|
|
193
|
+
"--sortr",
|
|
194
|
+
"--encoding",
|
|
195
|
+
"--pre",
|
|
196
|
+
"--pre-glob",
|
|
197
|
+
"--engine",
|
|
198
|
+
"--path-separator",
|
|
199
|
+
"--replace",
|
|
200
|
+
"--color",
|
|
201
|
+
"--colors",
|
|
202
|
+
"--ignore",
|
|
203
|
+
"--file-search-regex",
|
|
204
|
+
"--extension",
|
|
205
|
+
"--exclude",
|
|
206
|
+
"--exec",
|
|
207
|
+
"--exec-batch",
|
|
208
|
+
"--size",
|
|
209
|
+
"--changed-within",
|
|
210
|
+
"--changed-before",
|
|
211
|
+
}
|
|
212
|
+
),
|
|
213
|
+
all_depth_options=frozenset({"-d", "--max-depth", "--maxdepth", "--depth"}),
|
|
214
|
+
all_pattern_options=frozenset({"-e", "-f", "--regexp", "--file", "--files"}),
|
|
215
|
+
takes_pattern_positional=True,
|
|
216
|
+
)
|
|
217
|
+
UNIX_LS_GRAMMAR = PosixSearchGrammar(
|
|
218
|
+
is_recursive_by_default=False,
|
|
219
|
+
all_recurse_options=frozenset({"-R", "--recursive"}),
|
|
220
|
+
all_value_letters=frozenset({"I", "w", "T"}),
|
|
221
|
+
all_value_options=frozenset(
|
|
222
|
+
{"--ignore", "--hide", "--width", "--tabsize", "--sort", "--format", "--time-style"}
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
TREE_PROGRAM_NAME = "tree"
|
|
226
|
+
TREE_GRAMMAR = PosixSearchGrammar(
|
|
227
|
+
is_recursive_by_default=True,
|
|
228
|
+
all_value_letters=frozenset({"L", "P", "I", "o", "H", "T"}),
|
|
229
|
+
all_value_options=frozenset({"--filelimit", "--charset", "--sort", "--timefmt"}),
|
|
230
|
+
all_depth_options=frozenset({"-L"}),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
POWERSHELL_TOOL_NAME = "PowerShell"
|
|
234
|
+
POWERSHELL_DEPTH_OPTION = "-depth"
|
|
235
|
+
POWERSHELL_NON_RECURSIVE_DEPTH = "0"
|
|
236
|
+
ALL_POWERSHELL_SWITCH_OPTIONS: frozenset[str] = frozenset(
|
|
237
|
+
{
|
|
238
|
+
"-recurse",
|
|
239
|
+
"-r",
|
|
240
|
+
"-rec",
|
|
241
|
+
"-file",
|
|
242
|
+
"-directory",
|
|
243
|
+
"-ad",
|
|
244
|
+
"-af",
|
|
245
|
+
"-ah",
|
|
246
|
+
"-ar",
|
|
247
|
+
"-as",
|
|
248
|
+
"-force",
|
|
249
|
+
"-name",
|
|
250
|
+
"-hidden",
|
|
251
|
+
"-system",
|
|
252
|
+
"-readonly",
|
|
253
|
+
"-followsymlink",
|
|
254
|
+
"-list",
|
|
255
|
+
"-simplematch",
|
|
256
|
+
"-casesensitive",
|
|
257
|
+
"-notmatch",
|
|
258
|
+
"-allmatches",
|
|
259
|
+
"-quiet",
|
|
260
|
+
"-raw",
|
|
261
|
+
"-noemphasis",
|
|
262
|
+
"-wait",
|
|
263
|
+
"-verbose",
|
|
264
|
+
"-vb",
|
|
265
|
+
"-debug",
|
|
266
|
+
"-db",
|
|
267
|
+
"-whatif",
|
|
268
|
+
"-wi",
|
|
269
|
+
"-confirm",
|
|
270
|
+
"-cf",
|
|
271
|
+
"-usetransaction",
|
|
272
|
+
"-usetx",
|
|
273
|
+
}
|
|
274
|
+
)
|
|
275
|
+
POWERSHELL_GLUED_VALUE_SEPARATOR = ":"
|
|
276
|
+
POWERSHELL_MINIMUM_ABBREVIATION_LENGTH = 3
|
|
277
|
+
ALL_POWERSHELL_ABBREVIABLE_OPTIONS: frozenset[str] = ALL_POWERSHELL_SWITCH_OPTIONS | frozenset(
|
|
278
|
+
{POWERSHELL_DEPTH_OPTION, "-path", "-literalpath", "-filter", "-include", "-exclude"}
|
|
279
|
+
)
|
|
280
|
+
ALL_SELECT_STRING_PROGRAM_NAMES: frozenset[str] = frozenset({"select-string", "sls"})
|
|
281
|
+
|
|
282
|
+
GLOB_TOOL_NAME = "Glob"
|
|
283
|
+
ALL_FILE_SEARCH_TOOL_NAMES: frozenset[str] = frozenset({"Grep", GLOB_TOOL_NAME})
|
|
284
|
+
TOOL_INPUT_PATH_KEY = "path"
|
|
285
|
+
TOOL_INPUT_PATTERN_KEY = "pattern"
|
|
286
|
+
TOOL_INPUT_COMMAND_KEY = "command"
|
|
287
|
+
PAYLOAD_TOOL_NAME_KEY = "tool_name"
|
|
288
|
+
PAYLOAD_TOOL_INPUT_KEY = "tool_input"
|
|
289
|
+
PAYLOAD_CWD_KEY = "cwd"
|
|
290
|
+
|
|
291
|
+
GATE_HOOK_NAME = "transcript_root_search_gate.py"
|
|
292
|
+
|
|
293
|
+
TRANSCRIPT_ROOT_DENY_REASON_TEMPLATE = (
|
|
294
|
+
"BLOCKED: this call walks or searches every transcript under `{root}`. "
|
|
295
|
+
"A Claude transcript root holds thousands of JSONL session files, so a "
|
|
296
|
+
"recursive walk or content search there runs for minutes and times out. "
|
|
297
|
+
"Scope the search: list the project folders by name first, for example "
|
|
298
|
+
'`Get-ChildItem "{root}" -Directory -Filter "*<session id fragment>*"` or '
|
|
299
|
+
'`Glob(pattern="*<session id fragment>*", path="{root}")`, then read the one '
|
|
300
|
+
"transcript in the matching folder, or search inside that one folder."
|
|
301
|
+
)
|
package/package.json
CHANGED
|
@@ -392,15 +392,27 @@ def _unreadable_text(reading: Reading) -> str:
|
|
|
392
392
|
return f"{reading.account.name} ({reading.unread_reason})"
|
|
393
393
|
|
|
394
394
|
|
|
395
|
-
def
|
|
395
|
+
def _room_text(reading: Reading, is_spent: bool) -> str:
|
|
396
|
+
if is_spent or reading.meters is None:
|
|
397
|
+
return f"{reading.account.name} spent"
|
|
398
|
+
percent_left = reading.meters.tightest_percent_left
|
|
399
|
+
if percent_left is None:
|
|
400
|
+
return f"{reading.account.name} percent left unread"
|
|
401
|
+
return f"{reading.account.name} {percent_left:g}% left"
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _wait_reason(
|
|
405
|
+
all_room_texts: Sequence[str], all_unreadable_names: Sequence[str], is_every_account_unreadable: bool, soonest: _RetryTime
|
|
406
|
+
) -> str:
|
|
396
407
|
next_event = "reset" if soonest.is_known_reset else "check"
|
|
397
408
|
next_text = f"next {next_event} at {utc_time_text(soonest.at)}"
|
|
409
|
+
room_text = f" ({', '.join(all_room_texts)})" if all_room_texts else ""
|
|
398
410
|
if not all_unreadable_names:
|
|
399
|
-
return f"no account has room; {next_text}"
|
|
411
|
+
return f"no account has room{room_text}; {next_text}"
|
|
400
412
|
unreadable_text = ", ".join(all_unreadable_names)
|
|
401
413
|
if is_every_account_unreadable:
|
|
402
414
|
return f"no account meter could be read ({unreadable_text}); {next_text}"
|
|
403
|
-
return f"no readable account has room; meter unreadable for {unreadable_text}; {next_text}"
|
|
415
|
+
return f"no readable account has room{room_text}; meter unreadable for {unreadable_text}; {next_text}"
|
|
404
416
|
|
|
405
417
|
|
|
406
418
|
def _wait_decision(
|
|
@@ -423,5 +435,11 @@ def _wait_decision(
|
|
|
423
435
|
for each_reading in all_readings
|
|
424
436
|
if each_reading.meters is None and each_reading.account not in all_spent_accounts
|
|
425
437
|
]
|
|
438
|
+
all_room_texts = [
|
|
439
|
+
_room_text(each_reading, each_reading.account in all_spent_accounts)
|
|
440
|
+
for each_reading in all_readings
|
|
441
|
+
if each_reading.meters is not None or each_reading.account in all_spent_accounts
|
|
442
|
+
]
|
|
426
443
|
is_every_account_unreadable = len(all_unreadable_names) == len(all_readings)
|
|
427
|
-
|
|
444
|
+
reason = _wait_reason(all_room_texts, all_unreadable_names, is_every_account_unreadable, soonest)
|
|
445
|
+
return Decision("wait", None, soonest.at, reason, "wait")
|
|
@@ -86,6 +86,7 @@ ALL_ACTION_BOUNDARY_EXEMPT_REGISTRATION_PATHS = (
|
|
|
86
86
|
"blocking/resolve_thread_reply_gate.py",
|
|
87
87
|
"blocking/turn_review_gate.py",
|
|
88
88
|
"blocking/wakeup_cap_gate.py",
|
|
89
|
+
"blocking/transcript_root_search_gate.py",
|
|
89
90
|
)
|
|
90
91
|
ALL_TEST_DIRECTORY_NAMES = frozenset({"tests"})
|
|
91
92
|
ALL_TEST_FILE_PREFIXES = ("test_",)
|
|
@@ -91,7 +91,7 @@ def test_should_name_the_unread_account_and_its_reason_when_no_account_has_room(
|
|
|
91
91
|
|
|
92
92
|
assert decision.action == "wait"
|
|
93
93
|
assert decision.reason == (
|
|
94
|
-
"no readable account has room; meter unreadable for codex-3 (codex app-server failed: timed out); "
|
|
94
|
+
"no readable account has room (codex-4 0% left); meter unreadable for codex-3 (codex app-server failed: timed out); "
|
|
95
95
|
"next check at 2026-10-03T01:00:00+00:00"
|
|
96
96
|
)
|
|
97
97
|
assert payload[0]["unread_reason"] == "codex app-server failed: timed out"
|
|
@@ -542,21 +542,21 @@ _EV_CLAUDE = _account("ev", Product.CLAUDE)
|
|
|
542
542
|
pytest.param(
|
|
543
543
|
(Reading(_MAIN_CLAUDE, _meters(5, 50, short_reset=NOW + timedelta(minutes=30))), Reading(_EV_CLAUDE, None)),
|
|
544
544
|
{},
|
|
545
|
-
"no readable account has room; meter unreadable for ev; next reset at 2026-10-03T00:30:00+00:00",
|
|
545
|
+
"no readable account has room (main 5% left); meter unreadable for ev; next reset at 2026-10-03T00:30:00+00:00",
|
|
546
546
|
NOW + timedelta(minutes=30),
|
|
547
547
|
id="unreadable-beside-a-reset-before-the-check",
|
|
548
548
|
),
|
|
549
549
|
pytest.param(
|
|
550
550
|
(Reading(_MAIN_CLAUDE, _meters(5, 50, short_reset=NOW + timedelta(hours=2))), Reading(_EV_CLAUDE, None)),
|
|
551
551
|
{},
|
|
552
|
-
"no readable account has room; meter unreadable for ev; next check at 2026-10-03T01:00:00+00:00",
|
|
552
|
+
"no readable account has room (main 5% left); meter unreadable for ev; next check at 2026-10-03T01:00:00+00:00",
|
|
553
553
|
NOW + timedelta(hours=1),
|
|
554
554
|
id="unreadable-beside-a-reset-after-the-check",
|
|
555
555
|
),
|
|
556
556
|
pytest.param(
|
|
557
557
|
(Reading(_MAIN_CLAUDE, _meters(80, 80)), Reading(_EV_CLAUDE, None)),
|
|
558
558
|
{_MAIN_CLAUDE: NOW + timedelta(minutes=30)},
|
|
559
|
-
"no readable account has room; meter unreadable for ev; next reset at 2026-10-03T00:30:00+00:00",
|
|
559
|
+
"no readable account has room (main spent); meter unreadable for ev; next reset at 2026-10-03T00:30:00+00:00",
|
|
560
560
|
NOW + timedelta(minutes=30),
|
|
561
561
|
id="unreadable-beside-a-spent-account",
|
|
562
562
|
),
|
|
@@ -566,17 +566,31 @@ _EV_CLAUDE = _account("ev", Product.CLAUDE)
|
|
|
566
566
|
Reading(_EV_CLAUDE, _meters(5, 80, short_reset=NOW + timedelta(hours=3))),
|
|
567
567
|
),
|
|
568
568
|
{},
|
|
569
|
-
"no account has room; next reset at 2026-10-03T02:00:00+00:00",
|
|
569
|
+
"no account has room (main 5% left, ev 5% left); next reset at 2026-10-03T02:00:00+00:00",
|
|
570
570
|
NOW + timedelta(hours=2),
|
|
571
571
|
id="every-meter-read-with-known-resets",
|
|
572
572
|
),
|
|
573
573
|
pytest.param(
|
|
574
574
|
(Reading(_MAIN_CLAUDE, Meters(5, None, 50, None)),),
|
|
575
575
|
{},
|
|
576
|
-
"no account has room; next check at 2026-10-03T01:00:00+00:00",
|
|
576
|
+
"no account has room (main 5% left); next check at 2026-10-03T01:00:00+00:00",
|
|
577
577
|
NOW + timedelta(hours=1),
|
|
578
578
|
id="every-meter-read-without-a-known-reset",
|
|
579
579
|
),
|
|
580
|
+
pytest.param(
|
|
581
|
+
(),
|
|
582
|
+
{},
|
|
583
|
+
"no account has room; next check at 2026-10-03T01:00:00+00:00",
|
|
584
|
+
NOW + timedelta(hours=1),
|
|
585
|
+
id="no-account-in-the-roster",
|
|
586
|
+
),
|
|
587
|
+
pytest.param(
|
|
588
|
+
(Reading(_MAIN_CLAUDE, Meters(None, None, None, None)),),
|
|
589
|
+
{},
|
|
590
|
+
"no account has room (main percent left unread); next check at 2026-10-03T01:00:00+00:00",
|
|
591
|
+
NOW + timedelta(hours=1),
|
|
592
|
+
id="meter-read-without-a-percent",
|
|
593
|
+
),
|
|
580
594
|
),
|
|
581
595
|
)
|
|
582
596
|
def test_should_name_what_the_broker_knows_in_the_wait_reason(
|