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.
Files changed (77) hide show
  1. package/CLAUDE.md +0 -13
  2. package/_shared/pr-loop/scripts/code_rules_gate.py +106 -14
  3. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +26 -0
  4. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +160 -0
  5. package/agents/clean-coder.md +2 -2
  6. package/agents/code-verifier.md +0 -1
  7. package/agents/test_agent_frontmatter.py +78 -0
  8. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
  9. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +1 -1
  10. package/bin/install.mjs +1 -0
  11. package/docs/CODE_RULES.md +2 -2
  12. package/hooks/blocking/CLAUDE.md +6 -2
  13. package/hooks/blocking/code_rules_comments.py +2 -2
  14. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +44 -0
  15. package/hooks/blocking/config/verified_commit_constants.py +2 -0
  16. package/hooks/blocking/conftest.py +115 -0
  17. package/hooks/blocking/nas_ssh_binary_enforcer.py +191 -0
  18. package/hooks/blocking/pr_description_enforcer.py +46 -22
  19. package/hooks/blocking/pr_description_pr_number.py +5 -3
  20. package/hooks/blocking/pr_description_proof_of_work.py +367 -0
  21. package/hooks/blocking/precommit_code_rules_gate.py +5 -1
  22. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +8 -2
  23. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +71 -0
  24. package/hooks/blocking/test_nas_ssh_binary_enforcer.py +168 -0
  25. package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +175 -0
  26. package/hooks/blocking/test_pr_description_proof_of_work.py +162 -0
  27. package/hooks/blocking/test_precommit_code_rules_gate.py +89 -0
  28. package/hooks/blocking/test_verdict_directory_write_blocker.py +4 -0
  29. package/hooks/blocking/test_verification_verdict_store.py +11 -0
  30. package/hooks/blocking/test_verified_commit_config_bootstrap.py +49 -0
  31. package/hooks/blocking/test_verified_commit_gate.py +11 -0
  32. package/hooks/blocking/test_verifier_verdict_minter.py +11 -0
  33. package/hooks/blocking/test_volatile_path_in_post_blocker.py +210 -0
  34. package/hooks/blocking/verdict_directory_write_blocker.py +6 -0
  35. package/hooks/blocking/verification_verdict_store.py +73 -5
  36. package/hooks/blocking/verified_commit_config_bootstrap.py +51 -0
  37. package/hooks/blocking/verified_commit_gate.py +6 -0
  38. package/hooks/blocking/verifier_verdict_minter.py +6 -0
  39. package/hooks/blocking/volatile_path_in_post_blocker.py +351 -0
  40. package/hooks/hooks.json +32 -2
  41. package/hooks/hooks_constants/CLAUDE.md +4 -0
  42. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  43. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +3 -0
  44. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -0
  45. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +59 -0
  46. package/hooks/hooks_constants/pr_description_enforcer_constants.py +2 -0
  47. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +111 -0
  48. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +48 -0
  49. package/hooks/lifecycle/CLAUDE.md +3 -1
  50. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +146 -0
  51. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +178 -0
  52. package/hooks/validators/run_all_validators.py +216 -4
  53. package/hooks/validators/test_run_all_validators_pretooluse.py +102 -0
  54. package/package.json +1 -1
  55. package/rules/CLAUDE.md +3 -0
  56. package/rules/durable-post-artifacts.md +35 -0
  57. package/rules/nas-ssh-invocation.md +21 -0
  58. package/rules/proof-of-work-pr-comments.md +26 -0
  59. package/scripts/CLAUDE.md +2 -0
  60. package/scripts/claude-chain.example.json +8 -0
  61. package/scripts/claude_chain_runner.py +400 -0
  62. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
  63. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +124 -0
  64. package/scripts/dev_env_scripts_constants/gh_artifact_upload_constants.py +43 -0
  65. package/scripts/gh_artifact_upload.py +256 -0
  66. package/scripts/sync_to_cursor/rules.py +1 -1
  67. package/scripts/test_claude_chain_runner.py +472 -0
  68. package/scripts/tests/test_gh_artifact_upload.py +205 -0
  69. package/skills/CLAUDE.md +3 -0
  70. package/skills/team-advisor/SKILL.md +188 -0
  71. package/skills/team-advisor-refresh/SKILL.md +25 -0
  72. package/skills/usage-pause/SKILL.md +108 -0
  73. package/skills/usage-pause/scripts/resolve_usage_window.py +462 -0
  74. package/skills/usage-pause/scripts/test_resolve_usage_window.py +278 -0
  75. package/skills/usage-pause/scripts/usage_pause_constants/__init__.py +1 -0
  76. package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +65 -0
  77. 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 is absolute:
120
- - Every comment on an untouched line stays untouched
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