claude-dev-env 1.89.0 → 1.92.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/CLAUDE.md +0 -13
- package/_shared/pr-loop/scripts/code_rules_gate.py +106 -14
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +26 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +160 -0
- package/agents/clean-coder.md +2 -2
- package/agents/code-verifier.md +0 -1
- package/agents/test_agent_frontmatter.py +78 -0
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +1 -1
- package/bin/install.mjs +1 -0
- package/docs/CODE_RULES.md +2 -2
- package/hooks/blocking/CLAUDE.md +6 -2
- package/hooks/blocking/code_rules_comments.py +2 -2
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +44 -0
- package/hooks/blocking/config/verified_commit_constants.py +2 -0
- package/hooks/blocking/conftest.py +115 -0
- package/hooks/blocking/nas_ssh_binary_enforcer.py +191 -0
- package/hooks/blocking/pr_description_enforcer.py +46 -22
- package/hooks/blocking/pr_description_pr_number.py +5 -3
- package/hooks/blocking/pr_description_proof_of_work.py +367 -0
- package/hooks/blocking/precommit_code_rules_gate.py +5 -1
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +8 -2
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +71 -0
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +168 -0
- package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +175 -0
- package/hooks/blocking/test_pr_description_proof_of_work.py +162 -0
- package/hooks/blocking/test_precommit_code_rules_gate.py +89 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +4 -0
- package/hooks/blocking/test_verification_verdict_store.py +11 -0
- package/hooks/blocking/test_verified_commit_config_bootstrap.py +49 -0
- package/hooks/blocking/test_verified_commit_gate.py +11 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +11 -0
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +210 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +6 -0
- package/hooks/blocking/verification_verdict_store.py +73 -5
- package/hooks/blocking/verified_commit_config_bootstrap.py +51 -0
- package/hooks/blocking/verified_commit_gate.py +6 -0
- package/hooks/blocking/verifier_verdict_minter.py +6 -0
- package/hooks/blocking/volatile_path_in_post_blocker.py +351 -0
- package/hooks/hooks.json +32 -2
- package/hooks/hooks_constants/CLAUDE.md +4 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +3 -0
- package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -0
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +59 -0
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +2 -0
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +111 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +48 -0
- package/hooks/lifecycle/CLAUDE.md +3 -1
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +146 -0
- package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +178 -0
- package/hooks/validators/run_all_validators.py +216 -4
- package/hooks/validators/test_run_all_validators_pretooluse.py +102 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +3 -0
- package/rules/durable-post-artifacts.md +35 -0
- package/rules/nas-ssh-invocation.md +21 -0
- package/rules/proof-of-work-pr-comments.md +26 -0
- package/scripts/CLAUDE.md +2 -0
- package/scripts/claude-chain.example.json +8 -0
- package/scripts/claude_chain_runner.py +400 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +124 -0
- package/scripts/dev_env_scripts_constants/gh_artifact_upload_constants.py +43 -0
- package/scripts/gh_artifact_upload.py +256 -0
- package/scripts/sync_to_cursor/rules.py +1 -1
- package/scripts/test_claude_chain_runner.py +472 -0
- package/scripts/tests/test_gh_artifact_upload.py +205 -0
- package/skills/CLAUDE.md +3 -0
- package/skills/team-advisor/SKILL.md +188 -0
- package/skills/team-advisor-refresh/SKILL.md +25 -0
- package/skills/usage-pause/SKILL.md +108 -0
- package/skills/usage-pause/scripts/resolve_usage_window.py +462 -0
- package/skills/usage-pause/scripts/test_resolve_usage_window.py +278 -0
- package/skills/usage-pause/scripts/usage_pause_constants/__init__.py +1 -0
- package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +65 -0
- package/system-prompts/software-engineer.xml +3 -2
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Tests for the usage-pause window resolver."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import json
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from types import ModuleType
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
|
|
16
|
+
RESOLVER_PATH = SCRIPTS_DIRECTORY / "resolve_usage_window.py"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_resolver_module() -> ModuleType:
|
|
20
|
+
if str(SCRIPTS_DIRECTORY) not in sys.path:
|
|
21
|
+
sys.path.insert(0, str(SCRIPTS_DIRECTORY))
|
|
22
|
+
spec = importlib.util.spec_from_file_location("resolve_usage_window", RESOLVER_PATH)
|
|
23
|
+
assert spec is not None
|
|
24
|
+
assert spec.loader is not None
|
|
25
|
+
resolver_module = importlib.util.module_from_spec(spec)
|
|
26
|
+
sys.modules[spec.name] = resolver_module
|
|
27
|
+
spec.loader.exec_module(resolver_module)
|
|
28
|
+
return resolver_module
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def local_now() -> datetime:
|
|
32
|
+
return datetime(2026, 7, 8, 9, 0, 0).astimezone()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TestParseManualOverride:
|
|
36
|
+
def should_parse_clock_time_with_pm_meridiem_to_same_day(self) -> None:
|
|
37
|
+
resolver = load_resolver_module()
|
|
38
|
+
now = local_now()
|
|
39
|
+
reset_at = resolver.parse_manual_override("10:20pm", now)
|
|
40
|
+
assert reset_at.hour == 22
|
|
41
|
+
assert reset_at.minute == 20
|
|
42
|
+
assert reset_at.date() == now.date()
|
|
43
|
+
|
|
44
|
+
def should_roll_clock_time_to_next_day_when_already_past(self) -> None:
|
|
45
|
+
resolver = load_resolver_module()
|
|
46
|
+
now = datetime(2026, 7, 8, 23, 0, 0).astimezone()
|
|
47
|
+
reset_at = resolver.parse_manual_override("10:20pm", now)
|
|
48
|
+
assert reset_at.date() == (now + timedelta(days=1)).date()
|
|
49
|
+
assert reset_at.hour == 22
|
|
50
|
+
|
|
51
|
+
def should_parse_hour_only_clock_time_with_meridiem(self) -> None:
|
|
52
|
+
resolver = load_resolver_module()
|
|
53
|
+
reset_at = resolver.parse_manual_override("10pm", local_now())
|
|
54
|
+
assert reset_at.hour == 22
|
|
55
|
+
assert reset_at.minute == 0
|
|
56
|
+
|
|
57
|
+
def should_parse_twenty_four_hour_clock_time(self) -> None:
|
|
58
|
+
resolver = load_resolver_module()
|
|
59
|
+
reset_at = resolver.parse_manual_override("22:15", local_now())
|
|
60
|
+
assert reset_at.hour == 22
|
|
61
|
+
assert reset_at.minute == 15
|
|
62
|
+
|
|
63
|
+
def should_parse_am_clock_time(self) -> None:
|
|
64
|
+
resolver = load_resolver_module()
|
|
65
|
+
now = datetime(2026, 7, 8, 6, 0, 0).astimezone()
|
|
66
|
+
reset_at = resolver.parse_manual_override("9:05am", now)
|
|
67
|
+
assert reset_at.hour == 9
|
|
68
|
+
assert reset_at.minute == 5
|
|
69
|
+
|
|
70
|
+
def should_parse_minutes_duration(self) -> None:
|
|
71
|
+
resolver = load_resolver_module()
|
|
72
|
+
now = local_now()
|
|
73
|
+
reset_at = resolver.parse_manual_override("74m", now)
|
|
74
|
+
assert reset_at == now + timedelta(minutes=74)
|
|
75
|
+
|
|
76
|
+
def should_parse_bare_digits_as_minutes(self) -> None:
|
|
77
|
+
resolver = load_resolver_module()
|
|
78
|
+
now = local_now()
|
|
79
|
+
reset_at = resolver.parse_manual_override("45", now)
|
|
80
|
+
assert reset_at == now + timedelta(minutes=45)
|
|
81
|
+
|
|
82
|
+
def should_parse_hours_and_minutes_duration(self) -> None:
|
|
83
|
+
resolver = load_resolver_module()
|
|
84
|
+
now = local_now()
|
|
85
|
+
reset_at = resolver.parse_manual_override("1h30m", now)
|
|
86
|
+
assert reset_at == now + timedelta(minutes=90)
|
|
87
|
+
|
|
88
|
+
def should_parse_hours_only_duration(self) -> None:
|
|
89
|
+
resolver = load_resolver_module()
|
|
90
|
+
now = local_now()
|
|
91
|
+
reset_at = resolver.parse_manual_override("2h", now)
|
|
92
|
+
assert reset_at == now + timedelta(hours=2)
|
|
93
|
+
|
|
94
|
+
def should_reject_unparseable_override(self) -> None:
|
|
95
|
+
resolver = load_resolver_module()
|
|
96
|
+
with pytest.raises(ValueError):
|
|
97
|
+
resolver.parse_manual_override("soon", local_now())
|
|
98
|
+
|
|
99
|
+
def should_reject_meridiem_hour_out_of_range(self) -> None:
|
|
100
|
+
resolver = load_resolver_module()
|
|
101
|
+
with pytest.raises(ValueError):
|
|
102
|
+
resolver.parse_manual_override("13pm", local_now())
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TestPlanWakeupStages:
|
|
106
|
+
def should_split_long_wait_into_capped_stages_with_short_tail(self) -> None:
|
|
107
|
+
resolver = load_resolver_module()
|
|
108
|
+
stages = resolver.plan_wakeup_stages(4440)
|
|
109
|
+
assert stages == [3480, 960, 120]
|
|
110
|
+
|
|
111
|
+
def should_land_total_sleep_past_the_reset_by_the_tail_buffer(self) -> None:
|
|
112
|
+
resolver = load_resolver_module()
|
|
113
|
+
stages = resolver.plan_wakeup_stages(4440)
|
|
114
|
+
assert sum(stages) == 4440 + 120
|
|
115
|
+
|
|
116
|
+
def should_emit_single_stage_for_a_wait_shorter_than_tail_plus_minimum(
|
|
117
|
+
self,
|
|
118
|
+
) -> None:
|
|
119
|
+
resolver = load_resolver_module()
|
|
120
|
+
assert resolver.plan_wakeup_stages(30) == [150]
|
|
121
|
+
|
|
122
|
+
def should_fold_a_sub_minimum_leftover_into_the_tail(self) -> None:
|
|
123
|
+
resolver = load_resolver_module()
|
|
124
|
+
stages = resolver.plan_wakeup_stages(3510)
|
|
125
|
+
assert stages == [3480, 150]
|
|
126
|
+
|
|
127
|
+
def should_keep_every_stage_within_cap_and_minimum_across_a_sweep(self) -> None:
|
|
128
|
+
resolver = load_resolver_module()
|
|
129
|
+
for seconds_until_reset in range(0, 36000, 137):
|
|
130
|
+
stages = resolver.plan_wakeup_stages(seconds_until_reset)
|
|
131
|
+
assert stages, f"no stages for {seconds_until_reset}"
|
|
132
|
+
assert sum(stages) >= seconds_until_reset
|
|
133
|
+
for each_stage in stages:
|
|
134
|
+
assert 60 <= each_stage <= 3480
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class TestReadOauthAccessToken:
|
|
138
|
+
def write_credentials(self, target: Path, expires_at_milliseconds: int) -> None:
|
|
139
|
+
payload = {
|
|
140
|
+
"claudeAiOauth": {
|
|
141
|
+
"accessToken": "token-value",
|
|
142
|
+
"refreshToken": "refresh-value",
|
|
143
|
+
"expiresAt": expires_at_milliseconds,
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
target.write_text(json.dumps(payload), encoding="utf-8")
|
|
147
|
+
|
|
148
|
+
def should_return_token_when_not_expired(self, tmp_path: Path) -> None:
|
|
149
|
+
resolver = load_resolver_module()
|
|
150
|
+
now = local_now()
|
|
151
|
+
credentials_path = tmp_path / ".credentials.json"
|
|
152
|
+
future_milliseconds = int((now + timedelta(hours=1)).timestamp() * 1000)
|
|
153
|
+
self.write_credentials(credentials_path, future_milliseconds)
|
|
154
|
+
assert resolver.read_oauth_access_token(credentials_path, now) == "token-value"
|
|
155
|
+
|
|
156
|
+
def should_return_none_when_token_expired(self, tmp_path: Path) -> None:
|
|
157
|
+
resolver = load_resolver_module()
|
|
158
|
+
now = local_now()
|
|
159
|
+
credentials_path = tmp_path / ".credentials.json"
|
|
160
|
+
past_milliseconds = int((now - timedelta(hours=1)).timestamp() * 1000)
|
|
161
|
+
self.write_credentials(credentials_path, past_milliseconds)
|
|
162
|
+
assert resolver.read_oauth_access_token(credentials_path, now) is None
|
|
163
|
+
|
|
164
|
+
def should_return_none_when_file_missing(self, tmp_path: Path) -> None:
|
|
165
|
+
resolver = load_resolver_module()
|
|
166
|
+
missing_path = tmp_path / "absent.json"
|
|
167
|
+
assert resolver.read_oauth_access_token(missing_path, local_now()) is None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class TestExtractUsageWindows:
|
|
171
|
+
def should_extract_session_and_weekly_buckets(self) -> None:
|
|
172
|
+
resolver = load_resolver_module()
|
|
173
|
+
payload = {
|
|
174
|
+
"five_hour": {"utilization": 42, "resets_at": "2026-07-08T22:00:00+00:00"},
|
|
175
|
+
"seven_day": {"utilization": 63, "resets_at": "2026-07-12T00:00:00+00:00"},
|
|
176
|
+
}
|
|
177
|
+
windows = resolver.extract_usage_windows(payload)
|
|
178
|
+
assert isinstance(windows, resolver.UsageWindows)
|
|
179
|
+
assert windows.session_utilization == 42.0
|
|
180
|
+
assert windows.weekly_utilization == 63.0
|
|
181
|
+
assert windows.session_resets_at is not None
|
|
182
|
+
assert windows.weekly_resets_at is not None
|
|
183
|
+
|
|
184
|
+
def should_accept_epoch_seconds_resets_at(self) -> None:
|
|
185
|
+
resolver = load_resolver_module()
|
|
186
|
+
now = local_now()
|
|
187
|
+
epoch_seconds = int((now + timedelta(hours=2)).timestamp())
|
|
188
|
+
payload = {"five_hour": {"utilization": 10, "resets_at": epoch_seconds}}
|
|
189
|
+
windows = resolver.extract_usage_windows(payload)
|
|
190
|
+
assert windows.session_resets_at is not None
|
|
191
|
+
assert abs((windows.session_resets_at - now).total_seconds() - 7200) < 2
|
|
192
|
+
|
|
193
|
+
def should_handle_missing_buckets(self) -> None:
|
|
194
|
+
resolver = load_resolver_module()
|
|
195
|
+
windows = resolver.extract_usage_windows({})
|
|
196
|
+
assert windows.session_utilization is None
|
|
197
|
+
assert windows.session_resets_at is None
|
|
198
|
+
assert windows.weekly_utilization is None
|
|
199
|
+
assert windows.weekly_resets_at is None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class TestBuildPausePlan:
|
|
203
|
+
def should_flag_weekly_near_cap_at_the_threshold(self) -> None:
|
|
204
|
+
resolver = load_resolver_module()
|
|
205
|
+
now = local_now()
|
|
206
|
+
reset_at = now + timedelta(minutes=74)
|
|
207
|
+
resolved_payload = resolver.build_pause_plan(
|
|
208
|
+
"probe", reset_at, now, 50.0, 95.0, None
|
|
209
|
+
)
|
|
210
|
+
assert resolved_payload["weekly_near_cap"] is True
|
|
211
|
+
|
|
212
|
+
def should_not_flag_weekly_near_cap_below_the_threshold(self) -> None:
|
|
213
|
+
resolver = load_resolver_module()
|
|
214
|
+
now = local_now()
|
|
215
|
+
reset_at = now + timedelta(minutes=74)
|
|
216
|
+
resolved_payload = resolver.build_pause_plan(
|
|
217
|
+
"probe", reset_at, now, 50.0, 40.0, None
|
|
218
|
+
)
|
|
219
|
+
assert resolved_payload["weekly_near_cap"] is False
|
|
220
|
+
|
|
221
|
+
def should_carry_stage_plan_and_reset_time(self) -> None:
|
|
222
|
+
resolver = load_resolver_module()
|
|
223
|
+
now = local_now()
|
|
224
|
+
reset_at = now + timedelta(seconds=4440)
|
|
225
|
+
resolved_payload = resolver.build_pause_plan(
|
|
226
|
+
"override", reset_at, now, None, None, None
|
|
227
|
+
)
|
|
228
|
+
assert resolved_payload["source"] == "override"
|
|
229
|
+
assert resolved_payload["seconds_until_reset"] == 4440
|
|
230
|
+
assert resolved_payload["stages_seconds"] == [3480, 960, 120]
|
|
231
|
+
assert resolved_payload["reset_at"] == reset_at.isoformat()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class TestCommandLine:
|
|
235
|
+
def run_resolver(self, *arguments: str) -> subprocess.CompletedProcess[str]:
|
|
236
|
+
return subprocess.run(
|
|
237
|
+
[sys.executable, str(RESOLVER_PATH), *arguments],
|
|
238
|
+
capture_output=True,
|
|
239
|
+
text=True,
|
|
240
|
+
check=False,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def should_resolve_duration_override_end_to_end(self) -> None:
|
|
244
|
+
now = local_now()
|
|
245
|
+
completed = self.run_resolver("--override", "74m", "--now", now.isoformat())
|
|
246
|
+
assert completed.returncode == 0, completed.stderr
|
|
247
|
+
resolved_payload = json.loads(completed.stdout)
|
|
248
|
+
assert resolved_payload["source"] == "override"
|
|
249
|
+
assert resolved_payload["stages_seconds"] == [3480, 960, 120]
|
|
250
|
+
assert resolved_payload["weekly_near_cap"] is False
|
|
251
|
+
|
|
252
|
+
def should_resolve_clock_override_end_to_end(self) -> None:
|
|
253
|
+
now = datetime(2026, 7, 8, 21, 0, 0).astimezone()
|
|
254
|
+
completed = self.run_resolver("--override", "10:20pm", "--now", now.isoformat())
|
|
255
|
+
assert completed.returncode == 0, completed.stderr
|
|
256
|
+
resolved_payload = json.loads(completed.stdout)
|
|
257
|
+
assert resolved_payload["seconds_until_reset"] == 4800
|
|
258
|
+
|
|
259
|
+
def should_exit_with_probe_unavailable_code_when_credentials_missing(
|
|
260
|
+
self, tmp_path: Path
|
|
261
|
+
) -> None:
|
|
262
|
+
missing_path = tmp_path / "absent.json"
|
|
263
|
+
completed = self.run_resolver("--credentials-path", str(missing_path))
|
|
264
|
+
assert completed.returncode == 2
|
|
265
|
+
resolved_payload = json.loads(completed.stdout)
|
|
266
|
+
assert "error" in resolved_payload
|
|
267
|
+
|
|
268
|
+
def should_reject_invalid_override_with_error_payload(self) -> None:
|
|
269
|
+
completed = self.run_resolver("--override", "soon")
|
|
270
|
+
assert completed.returncode == 2
|
|
271
|
+
resolved_payload = json.loads(completed.stdout)
|
|
272
|
+
assert "error" in resolved_payload
|
|
273
|
+
|
|
274
|
+
def should_reject_invalid_now_with_error_payload(self) -> None:
|
|
275
|
+
completed = self.run_resolver("--override", "74m", "--now", "not-a-time")
|
|
276
|
+
assert completed.returncode == 2
|
|
277
|
+
resolved_payload = json.loads(completed.stdout)
|
|
278
|
+
assert "error" in resolved_payload
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Named constants for the usage-pause skill scripts."""
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Constants for the usage-window resolver.
|
|
2
|
+
|
|
3
|
+
Groups: the OAuth usage-endpoint probe, the CLI credential file keys, the
|
|
4
|
+
usage-response field keys, the override parse patterns, the wakeup stage
|
|
5
|
+
sizing, the weekly warn threshold, the result JSON keys, the source labels,
|
|
6
|
+
and the exit codes.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
OAUTH_USAGE_ENDPOINT_URL = "https://api.anthropic.com/api/oauth/usage"
|
|
12
|
+
OAUTH_BETA_HEADER_NAME = "anthropic-beta"
|
|
13
|
+
OAUTH_BETA_HEADER_VALUE = "oauth-2025-04-20"
|
|
14
|
+
AUTHORIZATION_HEADER_NAME = "Authorization"
|
|
15
|
+
AUTHORIZATION_BEARER_PREFIX = "Bearer "
|
|
16
|
+
CONTENT_TYPE_HEADER_NAME = "Content-Type"
|
|
17
|
+
CONTENT_TYPE_JSON = "application/json"
|
|
18
|
+
PROBE_TIMEOUT_SECONDS = 10
|
|
19
|
+
|
|
20
|
+
ALL_CREDENTIALS_RELATIVE_PATH_PARTS = (".claude", ".credentials.json")
|
|
21
|
+
CREDENTIALS_OAUTH_SECTION_KEY = "claudeAiOauth"
|
|
22
|
+
CREDENTIALS_ACCESS_TOKEN_KEY = "accessToken"
|
|
23
|
+
CREDENTIALS_EXPIRES_AT_KEY = "expiresAt"
|
|
24
|
+
MILLISECONDS_PER_SECOND = 1000
|
|
25
|
+
|
|
26
|
+
FIVE_HOUR_BUCKET_KEY = "five_hour"
|
|
27
|
+
SEVEN_DAY_BUCKET_KEY = "seven_day"
|
|
28
|
+
UTILIZATION_KEY = "utilization"
|
|
29
|
+
ALL_RESETS_AT_KEYS = ("resets_at", "resetsAt")
|
|
30
|
+
EPOCH_MILLISECONDS_THRESHOLD = 100_000_000_000
|
|
31
|
+
ISO_UTC_SUFFIX = "Z"
|
|
32
|
+
ISO_UTC_OFFSET = "+00:00"
|
|
33
|
+
|
|
34
|
+
DURATION_PATTERN = r"^(?:(?P<hours>\d+)h)?(?:(?P<minutes>\d+)m)?$"
|
|
35
|
+
BARE_MINUTES_PATTERN = r"^\d+$"
|
|
36
|
+
CLOCK_PATTERN = r"^(?P<hour>\d{1,2})(?::(?P<minute>\d{2}))?\s*(?P<meridiem>am|pm)?$"
|
|
37
|
+
MERIDIEM_PM = "pm"
|
|
38
|
+
MERIDIEM_AM = "am"
|
|
39
|
+
NOON_HOUR = 12
|
|
40
|
+
CLOCK_HOUR_MAXIMUM = 23
|
|
41
|
+
MINUTES_PER_HOUR = 60
|
|
42
|
+
|
|
43
|
+
MAXIMUM_STAGE_SECONDS = 3480
|
|
44
|
+
TAIL_STAGE_SECONDS = 120
|
|
45
|
+
MINIMUM_STAGE_SECONDS = 60
|
|
46
|
+
|
|
47
|
+
WEEKLY_UTILIZATION_WARN_THRESHOLD = 90.0
|
|
48
|
+
|
|
49
|
+
RESULT_KEY_SOURCE = "source"
|
|
50
|
+
RESULT_KEY_RESET_AT = "reset_at"
|
|
51
|
+
RESULT_KEY_SECONDS_UNTIL_RESET = "seconds_until_reset"
|
|
52
|
+
RESULT_KEY_STAGES_SECONDS = "stages_seconds"
|
|
53
|
+
RESULT_KEY_SESSION_UTILIZATION = "session_utilization"
|
|
54
|
+
RESULT_KEY_WEEKLY_UTILIZATION = "weekly_utilization"
|
|
55
|
+
RESULT_KEY_WEEKLY_RESETS_AT = "weekly_resets_at"
|
|
56
|
+
RESULT_KEY_WEEKLY_NEAR_CAP = "weekly_near_cap"
|
|
57
|
+
RESULT_KEY_ERROR = "error"
|
|
58
|
+
|
|
59
|
+
SOURCE_PROBE = "probe"
|
|
60
|
+
SOURCE_OVERRIDE = "override"
|
|
61
|
+
|
|
62
|
+
EXIT_CODE_RESOLVED = 0
|
|
63
|
+
EXIT_CODE_PROBE_UNAVAILABLE = 2
|
|
64
|
+
|
|
65
|
+
LOGGING_FORMAT = "%(levelname)s %(name)s: %(message)s"
|
|
@@ -116,8 +116,9 @@
|
|
|
116
116
|
- Search config/ before defining any constant; import existing constants before creating new ones
|
|
117
117
|
- UPPER_SNAKE names belong in config/ only
|
|
118
118
|
|
|
119
|
-
Comments — preservation
|
|
120
|
-
-
|
|
119
|
+
Comments — preservation, add-time blocking:
|
|
120
|
+
- Keep every comment on an untouched line untouched; removing one prints a stderr advisory and lets the edit through
|
|
121
|
+
- Adding a new inline comment in production code blocks the edit
|
|
121
122
|
- Express new explanations through self-documenting names
|
|
122
123
|
- Docstrings on new files, methods, and classes are permitted
|
|
123
124
|
|