claude-dev-env 1.92.0 → 1.93.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 +5 -39
- package/_shared/CLAUDE.md +2 -1
- package/_shared/advisor/CLAUDE.md +21 -0
- package/_shared/advisor/advisor-protocol.md +142 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/__init__.py +1 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +86 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +246 -0
- package/_shared/advisor/scripts/pyproject.toml +3 -0
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +302 -0
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +180 -0
- package/_shared/advisor/scripts/tier_model_ids.py +164 -0
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/precatch-rubric.md +65 -0
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/code_rules_gate.py +137 -5
- package/_shared/pr-loop/scripts/post_audit_thread.py +1 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +16 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +2 -1
- package/_shared/pr-loop/scripts/reviewer_availability.py +42 -12
- package/_shared/pr-loop/scripts/reviews_disabled.py +65 -21
- package/_shared/pr-loop/scripts/terminology_sweep.py +69 -21
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/tests/fixtures/{copilot_internal_user_jonecho.json → copilot_internal_user_example.json} +1 -1
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +1 -1
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +146 -2
- package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +8 -8
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +18 -5
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +56 -5
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +104 -17
- package/agents/CLAUDE.md +3 -2
- package/agents/clasp-deployment-orchestrator.md +2 -2
- package/agents/code-advisor.md +7 -5
- package/agents/code-verifier.md +6 -2
- package/agents/session-advisor.md +27 -0
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
- package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -1
- package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/audit-rubrics/prompts/category-c-resource-cleanup.md +2 -2
- package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +2 -2
- package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
- package/audit-rubrics/prompts/category-f-silent-failures.md +2 -2
- package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
- package/audit-rubrics/prompts/category-h-security-boundaries.md +2 -2
- package/audit-rubrics/prompts/category-i-concurrency.md +2 -2
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +2 -2
- package/audit-rubrics/prompts/category-k-codebase-conflicts.md +2 -2
- package/audit-rubrics/prompts/category-l-behavior-equivalence.md +2 -2
- package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +2 -2
- package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +2 -2
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +2 -2
- package/bin/CLAUDE.md +1 -0
- package/bin/expand_home_directory_tokens.mjs +75 -0
- package/bin/install.mjs +21 -7
- package/bin/install.test.mjs +220 -10
- package/hooks/CLAUDE.md +1 -1
- package/hooks/blocking/CLAUDE.md +4 -1
- package/hooks/blocking/_path_setup.py +13 -0
- package/hooks/blocking/bash_pre_tool_use_dispatcher.py +281 -0
- package/hooks/blocking/code_rules_dead_module_constant.py +88 -16
- package/hooks/blocking/code_rules_enforcer.py +6 -0
- package/hooks/blocking/code_rules_imports_logging.py +34 -1
- package/hooks/blocking/code_rules_paths_syspath.py +1 -1
- package/hooks/blocking/code_rules_test_layout.py +272 -0
- package/hooks/blocking/nas_ssh_binary_enforcer.py +22 -12
- package/hooks/blocking/pii_prevention_blocker.py +781 -0
- package/hooks/blocking/pii_scanner.py +340 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
- package/hooks/blocking/stop_dispatcher.py +84 -0
- package/hooks/blocking/test__path_setup.py +39 -0
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +61 -0
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +294 -0
- package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +51 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +103 -0
- package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +11 -11
- package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_test_layout.py +111 -0
- package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +76 -0
- package/hooks/blocking/test_destructive_command_blocker.py +29 -9
- package/hooks/blocking/test_gh_body_arg_blocker.py +3 -3
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +96 -39
- package/hooks/blocking/test_pii_prevention_blocker.py +905 -0
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +41 -0
- package/hooks/blocking/test_pii_scanner.py +165 -0
- package/hooks/blocking/test_pr_description_enforcer_pr_number.py +1 -1
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +14 -13
- package/hooks/blocking/test_stop_dispatcher.py +187 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +1 -1
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +3 -3
- package/hooks/blocking/volatile_path_in_post_blocker.py +54 -5
- package/hooks/diagnostic/migrations/README.md +25 -26
- package/hooks/diagnostic/test_hook_log_extractor.py +10 -10
- package/hooks/hooks.json +10 -120
- package/hooks/hooks_constants/CLAUDE.md +12 -3
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +78 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +1 -0
- package/hooks/hooks_constants/hosted_hook_runner.py +73 -0
- package/hooks/hooks_constants/local_identity.py +182 -0
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +4 -37
- package/hooks/hooks_constants/pii_prevention_constants.py +299 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/python_style_checks_constants.py +15 -0
- package/hooks/hooks_constants/stop_dispatcher_constants.py +26 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +87 -0
- package/hooks/hooks_constants/test_hardcoded_user_path_constants.py +2 -2
- package/hooks/hooks_constants/test_hosted_hook_runner.py +101 -0
- package/hooks/hooks_constants/test_layout_constants.py +28 -0
- package/hooks/hooks_constants/test_local_identity.py +88 -0
- package/hooks/hooks_constants/test_session_env_cleanup_constants.py +1 -1
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +32 -0
- package/hooks/validators/README.md +5 -6
- package/hooks/validators/__init__.py +2 -2
- package/hooks/validators/python_style_checks.py +298 -243
- package/hooks/validators/run_all_validators.py +6 -0
- package/hooks/validators/test_python_style_checks.py +278 -163
- package/package.json +2 -2
- package/rules/CLAUDE.md +7 -1
- package/rules/bdd.md +1 -1
- package/rules/nas-ssh-invocation.md +6 -4
- package/rules/no-justification-noise.md +61 -0
- package/rules/testing.md +0 -2
- package/scripts/test_setup_project_paths.py +1 -1
- package/skills/CLAUDE.md +6 -3
- package/skills/_shared/pr-loop/scripts/_path_resolver.py +2 -2
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/test__path_resolver.py +42 -3
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +11 -11
- package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +26 -3
- package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +49 -25
- package/skills/autoconverge/CLAUDE.md +3 -3
- package/skills/autoconverge/SKILL.md +113 -35
- package/skills/autoconverge/reference/CLAUDE.md +2 -2
- package/skills/autoconverge/reference/convergence.md +44 -22
- package/skills/autoconverge/reference/gotchas.md +11 -0
- package/skills/autoconverge/reference/stop-conditions.md +23 -13
- package/skills/autoconverge/workflow/CLAUDE.md +2 -1
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +15 -19
- package/skills/autoconverge/workflow/converge.contract.test.mjs +43 -42
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +141 -16
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +2 -2
- package/skills/autoconverge/workflow/converge.mjs +329 -68
- package/skills/autoconverge/workflow/converge.path-aware.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.precatch.test.mjs +152 -0
- package/skills/autoconverge/workflow/converge.run-input.test.mjs +3 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +15 -15
- package/skills/autoconverge/workflow/test_convergence_summary.py +6 -6
- package/skills/bdd-protocol/SKILL.md +4 -5
- package/skills/bdd-protocol/references/anti-patterns.md +1 -1
- package/skills/bugteam/SKILL.md +8 -0
- package/skills/bugteam/reference/copilot-gap-analysis.md +20 -20
- package/skills/bugteam/reference/obstacles/audit-walk-categories.md +1 -1
- package/skills/bugteam/reference/team-setup.md +7 -5
- package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
- package/skills/copilot-finding-triage/SKILL.md +124 -0
- package/skills/copilot-finding-triage/reference/tier-rubric.md +49 -0
- package/skills/copilot-finding-triage/scripts/conftest.py +8 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/__init__.py +0 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/__init__.py +0 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/notify_ntfy_constants.py +32 -0
- package/skills/copilot-finding-triage/scripts/notify_ntfy.py +192 -0
- package/skills/copilot-finding-triage/scripts/test_notify_ntfy.py +141 -0
- package/skills/copilot-finding-triage/templates/notification.md +34 -0
- package/skills/copilot-review/SKILL.md +8 -6
- package/skills/findbugs/SKILL.md +4 -0
- package/skills/fixbugs/SKILL.md +8 -7
- package/skills/gotcha/CLAUDE.md +2 -2
- package/skills/gotcha/SKILL.md +4 -4
- package/skills/log-audit/SKILL.md +4 -6
- package/skills/monitor-open-prs/CLAUDE.md +1 -1
- package/skills/monitor-open-prs/SKILL.md +6 -1
- package/skills/orchestrator/SKILL.md +184 -0
- package/skills/orchestrator-refresh/SKILL.md +37 -0
- package/skills/post-audit-findings/SKILL.md +5 -9
- package/skills/pr-consistency-audit/SKILL.md +5 -1
- package/skills/pr-converge/CLAUDE.md +1 -1
- package/skills/pr-converge/SKILL.md +86 -47
- package/skills/pr-converge/reference/CLAUDE.md +1 -1
- package/skills/pr-converge/reference/convergence-gates.md +20 -17
- package/skills/pr-converge/reference/examples.md +63 -47
- package/skills/pr-converge/reference/fix-protocol.md +15 -15
- package/skills/pr-converge/reference/ground-rules.md +11 -7
- package/skills/pr-converge/reference/multi-pr-orchestration.md +6 -5
- package/skills/pr-converge/reference/per-tick.md +129 -107
- package/skills/pr-converge/reference/state-schema.md +15 -10
- package/skills/pr-converge/scripts/check_convergence.py +5 -4
- package/skills/pr-converge/scripts/test_check_convergence.py +6 -4
- package/skills/pr-fix-protocol/SKILL.md +3 -8
- package/skills/pr-loop-cloud-transport/SKILL.md +102 -0
- package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +40 -0
- package/skills/pr-loop-cloud-transport/reference/substitution-matrix.md +48 -0
- package/skills/pr-loop-lifecycle/SKILL.md +5 -9
- package/skills/pr-scope-resolve/SKILL.md +3 -6
- package/skills/privacy-hygiene/SKILL.md +114 -0
- package/skills/qbug/SKILL.md +8 -8
- package/skills/rebase/SKILL.md +5 -1
- package/skills/refine/SKILL.md +4 -5
- package/skills/reviewer-gates/SKILL.md +7 -11
- package/skills/session-log/SKILL.md +4 -1
- package/skills/skill-builder/SKILL.md +3 -6
- package/skills/structure-prompt/SKILL.md +4 -5
- package/skills/team-advisor/SKILL.md +32 -164
- package/skills/test_markdown_link_integrity.py +10 -6
- package/skills/update/SKILL.md +5 -1
- package/skills/usage-pause/SKILL.md +14 -5
- package/skills/usage-pause/scripts/resolve_usage_window.py +83 -5
- package/skills/usage-pause/scripts/test_resolve_usage_window.py +185 -17
- package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +4 -3
- package/skills/verified-build/SKILL.md +4 -9
- package/skills/team-advisor-refresh/SKILL.md +0 -25
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
{"source": "override", "reset_at": "2026-07-08T10:14:00-07:00",
|
|
8
8
|
"seconds_until_reset": 4440, "stages_seconds": [3480, 960, 120], ...}
|
|
9
9
|
|
|
10
|
-
With no ``--override``, the script
|
|
11
|
-
from the CLI credential file
|
|
10
|
+
With no ``--override``, the script resolves a bearer token: the Claude Code
|
|
11
|
+
OAuth access token from the CLI credential file, or the session ingress token
|
|
12
|
+
file when that credential is unavailable. It asks the OAuth usage endpoint for the
|
|
12
13
|
``five_hour`` and ``seven_day`` windows. Exit code 2 means the probe cannot
|
|
13
14
|
resolve; the caller then asks the user for a manual reset time.
|
|
14
15
|
"""
|
|
@@ -18,6 +19,7 @@ from __future__ import annotations
|
|
|
18
19
|
import argparse
|
|
19
20
|
import json
|
|
20
21
|
import logging
|
|
22
|
+
import os
|
|
21
23
|
import re
|
|
22
24
|
import sys
|
|
23
25
|
import urllib.error
|
|
@@ -67,6 +69,7 @@ from usage_pause_constants.resolve_usage_window_constants import (
|
|
|
67
69
|
RESULT_KEY_WEEKLY_NEAR_CAP,
|
|
68
70
|
RESULT_KEY_WEEKLY_RESETS_AT,
|
|
69
71
|
RESULT_KEY_WEEKLY_UTILIZATION,
|
|
72
|
+
SESSION_INGRESS_TOKEN_FILE_ENV_VAR,
|
|
70
73
|
SEVEN_DAY_BUCKET_KEY,
|
|
71
74
|
SOURCE_OVERRIDE,
|
|
72
75
|
SOURCE_PROBE,
|
|
@@ -212,6 +215,8 @@ def read_oauth_access_token(credentials_path: Path, now: datetime) -> str | None
|
|
|
212
215
|
"""
|
|
213
216
|
try:
|
|
214
217
|
credentials_payload = json.loads(credentials_path.read_text(encoding="utf-8"))
|
|
218
|
+
except FileNotFoundError:
|
|
219
|
+
return None
|
|
215
220
|
except (OSError, json.JSONDecodeError):
|
|
216
221
|
logger.warning("credential file unreadable at %s", credentials_path)
|
|
217
222
|
return None
|
|
@@ -234,6 +239,54 @@ def read_oauth_access_token(credentials_path: Path, now: datetime) -> str | None
|
|
|
234
239
|
return access_token
|
|
235
240
|
|
|
236
241
|
|
|
242
|
+
def read_session_ingress_token() -> str | None:
|
|
243
|
+
"""Read the session ingress bearer token from its named file.
|
|
244
|
+
|
|
245
|
+
::
|
|
246
|
+
|
|
247
|
+
(env var names /run/token) -> "<token text>"
|
|
248
|
+
(env var unset) -> None
|
|
249
|
+
|
|
250
|
+
Returns None when the environment variable is unset, and None with a
|
|
251
|
+
warning naming the path when the named file is unreadable or empty.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
The bearer token for the usage endpoint, or None when unavailable.
|
|
255
|
+
"""
|
|
256
|
+
token_file_path = os.environ.get(SESSION_INGRESS_TOKEN_FILE_ENV_VAR)
|
|
257
|
+
if not token_file_path:
|
|
258
|
+
return None
|
|
259
|
+
try:
|
|
260
|
+
token_text = Path(token_file_path).read_text(encoding="utf-8").strip()
|
|
261
|
+
except (OSError, UnicodeDecodeError):
|
|
262
|
+
logger.warning("session ingress token file unreadable at %s", token_file_path)
|
|
263
|
+
return None
|
|
264
|
+
if not token_text:
|
|
265
|
+
logger.warning("session ingress token file empty at %s", token_file_path)
|
|
266
|
+
return None
|
|
267
|
+
return token_text
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def resolve_access_token(credentials_path: Path, now: datetime) -> str | None:
|
|
271
|
+
"""Choose the usage-endpoint bearer token from its available sources.
|
|
272
|
+
|
|
273
|
+
::
|
|
274
|
+
|
|
275
|
+
credential file token valid -> the credential token
|
|
276
|
+
credential token unavailable -> the session ingress token
|
|
277
|
+
neither source available -> None
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
credentials_path: The CLI credential file holding the OAuth section.
|
|
281
|
+
now: The current time the stored credential expiry is compared against.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
The bearer token for the usage endpoint, or None when no source has one.
|
|
285
|
+
"""
|
|
286
|
+
credential_token = read_oauth_access_token(credentials_path, now)
|
|
287
|
+
return credential_token or read_session_ingress_token()
|
|
288
|
+
|
|
289
|
+
|
|
237
290
|
def _parse_resets_at(raw_resets_at: object) -> datetime | None:
|
|
238
291
|
if isinstance(raw_resets_at, bool):
|
|
239
292
|
return None
|
|
@@ -370,6 +423,30 @@ def _emit_error(message: str) -> int:
|
|
|
370
423
|
return EXIT_CODE_PROBE_UNAVAILABLE
|
|
371
424
|
|
|
372
425
|
|
|
426
|
+
def _describe_ingress_token_source() -> str:
|
|
427
|
+
"""Name the session ingress token source for the no-token error.
|
|
428
|
+
|
|
429
|
+
::
|
|
430
|
+
|
|
431
|
+
(env var holds /run/token) -> "the session ingress token file at /run/token"
|
|
432
|
+
(env var unset) -> "the session ingress token file (... unset)"
|
|
433
|
+
(env var set but empty) -> "the session ingress token file (... set but empty)"
|
|
434
|
+
|
|
435
|
+
The operator reads which file the ingress lookup tried, learns the
|
|
436
|
+
environment variable was never set, or learns it was set to an empty value.
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
A clause naming the ingress token file path, the unset variable, or the
|
|
440
|
+
set-but-empty variable.
|
|
441
|
+
"""
|
|
442
|
+
ingress_token_file_path = os.environ.get(SESSION_INGRESS_TOKEN_FILE_ENV_VAR)
|
|
443
|
+
if ingress_token_file_path:
|
|
444
|
+
return f"the session ingress token file at {ingress_token_file_path}"
|
|
445
|
+
if ingress_token_file_path is None:
|
|
446
|
+
return f"the session ingress token file ({SESSION_INGRESS_TOKEN_FILE_ENV_VAR} unset)"
|
|
447
|
+
return f"the session ingress token file ({SESSION_INGRESS_TOKEN_FILE_ENV_VAR} set but empty)"
|
|
448
|
+
|
|
449
|
+
|
|
373
450
|
def _parse_arguments() -> argparse.Namespace:
|
|
374
451
|
parser = argparse.ArgumentParser(
|
|
375
452
|
description="Resolve the 5-hour usage window reset and plan the pause stage chain.",
|
|
@@ -424,11 +501,12 @@ def main() -> int:
|
|
|
424
501
|
if arguments.credentials_path
|
|
425
502
|
else Path.home().joinpath(*ALL_CREDENTIALS_RELATIVE_PATH_PARTS)
|
|
426
503
|
)
|
|
427
|
-
access_token =
|
|
504
|
+
access_token = resolve_access_token(credentials_path, now)
|
|
428
505
|
if access_token is None:
|
|
429
506
|
return _emit_error(
|
|
430
|
-
"
|
|
431
|
-
"
|
|
507
|
+
"no usable bearer token from the OAuth credential file at "
|
|
508
|
+
f"{credentials_path} or {_describe_ingress_token_source()}; give a "
|
|
509
|
+
"manual reset time, for example /usage-pause 10:20pm or /usage-pause 74m"
|
|
432
510
|
)
|
|
433
511
|
try:
|
|
434
512
|
usage_payload = _fetch_usage_payload(access_token)
|
|
@@ -4,6 +4,8 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import importlib.util
|
|
6
6
|
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
7
9
|
import subprocess
|
|
8
10
|
import sys
|
|
9
11
|
from datetime import datetime, timedelta
|
|
@@ -13,12 +15,17 @@ from types import ModuleType
|
|
|
13
15
|
import pytest
|
|
14
16
|
|
|
15
17
|
SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
|
|
18
|
+
if str(SCRIPTS_DIRECTORY) not in sys.path:
|
|
19
|
+
sys.path.insert(0, str(SCRIPTS_DIRECTORY))
|
|
20
|
+
|
|
21
|
+
from usage_pause_constants.resolve_usage_window_constants import (
|
|
22
|
+
SESSION_INGRESS_TOKEN_FILE_ENV_VAR as INGRESS_TOKEN_FILE_ENV_VAR,
|
|
23
|
+
)
|
|
24
|
+
|
|
16
25
|
RESOLVER_PATH = SCRIPTS_DIRECTORY / "resolve_usage_window.py"
|
|
17
26
|
|
|
18
27
|
|
|
19
28
|
def load_resolver_module() -> ModuleType:
|
|
20
|
-
if str(SCRIPTS_DIRECTORY) not in sys.path:
|
|
21
|
-
sys.path.insert(0, str(SCRIPTS_DIRECTORY))
|
|
22
29
|
spec = importlib.util.spec_from_file_location("resolve_usage_window", RESOLVER_PATH)
|
|
23
30
|
assert spec is not None
|
|
24
31
|
assert spec.loader is not None
|
|
@@ -32,6 +39,19 @@ def local_now() -> datetime:
|
|
|
32
39
|
return datetime(2026, 7, 8, 9, 0, 0).astimezone()
|
|
33
40
|
|
|
34
41
|
|
|
42
|
+
def write_credentials(
|
|
43
|
+
target: Path, expires_at_milliseconds: int, access_token: str = "token-value"
|
|
44
|
+
) -> None:
|
|
45
|
+
payload = {
|
|
46
|
+
"claudeAiOauth": {
|
|
47
|
+
"accessToken": access_token,
|
|
48
|
+
"refreshToken": "refresh-value",
|
|
49
|
+
"expiresAt": expires_at_milliseconds,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
target.write_text(json.dumps(payload), encoding="utf-8")
|
|
53
|
+
|
|
54
|
+
|
|
35
55
|
class TestParseManualOverride:
|
|
36
56
|
def should_parse_clock_time_with_pm_meridiem_to_same_day(self) -> None:
|
|
37
57
|
resolver = load_resolver_module()
|
|
@@ -135,22 +155,12 @@ class TestPlanWakeupStages:
|
|
|
135
155
|
|
|
136
156
|
|
|
137
157
|
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
158
|
def should_return_token_when_not_expired(self, tmp_path: Path) -> None:
|
|
149
159
|
resolver = load_resolver_module()
|
|
150
160
|
now = local_now()
|
|
151
161
|
credentials_path = tmp_path / ".credentials.json"
|
|
152
162
|
future_milliseconds = int((now + timedelta(hours=1)).timestamp() * 1000)
|
|
153
|
-
|
|
163
|
+
write_credentials(credentials_path, future_milliseconds)
|
|
154
164
|
assert resolver.read_oauth_access_token(credentials_path, now) == "token-value"
|
|
155
165
|
|
|
156
166
|
def should_return_none_when_token_expired(self, tmp_path: Path) -> None:
|
|
@@ -158,13 +168,131 @@ class TestReadOauthAccessToken:
|
|
|
158
168
|
now = local_now()
|
|
159
169
|
credentials_path = tmp_path / ".credentials.json"
|
|
160
170
|
past_milliseconds = int((now - timedelta(hours=1)).timestamp() * 1000)
|
|
161
|
-
|
|
171
|
+
write_credentials(credentials_path, past_milliseconds)
|
|
162
172
|
assert resolver.read_oauth_access_token(credentials_path, now) is None
|
|
163
173
|
|
|
164
|
-
def
|
|
174
|
+
def should_not_warn_when_credential_file_missing(
|
|
175
|
+
self, tmp_path: Path, caplog: pytest.LogCaptureFixture
|
|
176
|
+
) -> None:
|
|
165
177
|
resolver = load_resolver_module()
|
|
166
178
|
missing_path = tmp_path / "absent.json"
|
|
167
|
-
|
|
179
|
+
with caplog.at_level(logging.WARNING):
|
|
180
|
+
assert resolver.read_oauth_access_token(missing_path, local_now()) is None
|
|
181
|
+
assert caplog.records == []
|
|
182
|
+
|
|
183
|
+
def should_warn_when_credential_file_present_but_corrupt(
|
|
184
|
+
self, tmp_path: Path, caplog: pytest.LogCaptureFixture
|
|
185
|
+
) -> None:
|
|
186
|
+
resolver = load_resolver_module()
|
|
187
|
+
credentials_path = tmp_path / ".credentials.json"
|
|
188
|
+
credentials_path.write_text("{not valid json", encoding="utf-8")
|
|
189
|
+
with caplog.at_level(logging.WARNING):
|
|
190
|
+
assert (
|
|
191
|
+
resolver.read_oauth_access_token(credentials_path, local_now())
|
|
192
|
+
is None
|
|
193
|
+
)
|
|
194
|
+
assert any("unreadable" in each_message for each_message in caplog.messages)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class TestReadSessionIngressToken:
|
|
198
|
+
def should_return_stripped_token_from_the_named_file(
|
|
199
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
200
|
+
) -> None:
|
|
201
|
+
resolver = load_resolver_module()
|
|
202
|
+
token_file = tmp_path / "ingress-token"
|
|
203
|
+
token_file.write_text(" ingress-token-value\n", encoding="utf-8")
|
|
204
|
+
monkeypatch.setenv(INGRESS_TOKEN_FILE_ENV_VAR, str(token_file))
|
|
205
|
+
assert resolver.read_session_ingress_token() == "ingress-token-value"
|
|
206
|
+
|
|
207
|
+
def should_return_none_when_env_var_unset(
|
|
208
|
+
self, monkeypatch: pytest.MonkeyPatch
|
|
209
|
+
) -> None:
|
|
210
|
+
resolver = load_resolver_module()
|
|
211
|
+
monkeypatch.delenv(INGRESS_TOKEN_FILE_ENV_VAR, raising=False)
|
|
212
|
+
assert resolver.read_session_ingress_token() is None
|
|
213
|
+
|
|
214
|
+
def should_return_none_when_named_file_missing(
|
|
215
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
216
|
+
) -> None:
|
|
217
|
+
resolver = load_resolver_module()
|
|
218
|
+
monkeypatch.setenv(INGRESS_TOKEN_FILE_ENV_VAR, str(tmp_path / "absent-token"))
|
|
219
|
+
assert resolver.read_session_ingress_token() is None
|
|
220
|
+
|
|
221
|
+
def should_return_none_when_file_is_empty(
|
|
222
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
223
|
+
) -> None:
|
|
224
|
+
resolver = load_resolver_module()
|
|
225
|
+
token_file = tmp_path / "ingress-token"
|
|
226
|
+
token_file.write_text(" \n", encoding="utf-8")
|
|
227
|
+
monkeypatch.setenv(INGRESS_TOKEN_FILE_ENV_VAR, str(token_file))
|
|
228
|
+
assert resolver.read_session_ingress_token() is None
|
|
229
|
+
|
|
230
|
+
def should_return_none_when_file_is_not_valid_utf8(
|
|
231
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
232
|
+
) -> None:
|
|
233
|
+
resolver = load_resolver_module()
|
|
234
|
+
token_file = tmp_path / "ingress-token"
|
|
235
|
+
token_file.write_bytes(b"\xff\xfe\x00token")
|
|
236
|
+
monkeypatch.setenv(INGRESS_TOKEN_FILE_ENV_VAR, str(token_file))
|
|
237
|
+
assert resolver.read_session_ingress_token() is None
|
|
238
|
+
|
|
239
|
+
def should_pin_the_env_var_name_to_the_platform_contract(self) -> None:
|
|
240
|
+
assert INGRESS_TOKEN_FILE_ENV_VAR == "CLAUDE_SESSION_INGRESS_TOKEN_FILE"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class TestResolveAccessToken:
|
|
244
|
+
def should_use_ingress_token_when_credential_file_missing(
|
|
245
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
246
|
+
) -> None:
|
|
247
|
+
resolver = load_resolver_module()
|
|
248
|
+
missing_credentials = tmp_path / "absent.json"
|
|
249
|
+
token_file = tmp_path / "ingress-token"
|
|
250
|
+
token_file.write_text("ingress-token-value", encoding="utf-8")
|
|
251
|
+
monkeypatch.setenv(INGRESS_TOKEN_FILE_ENV_VAR, str(token_file))
|
|
252
|
+
chosen_token = resolver.resolve_access_token(missing_credentials, local_now())
|
|
253
|
+
assert chosen_token == "ingress-token-value"
|
|
254
|
+
|
|
255
|
+
def should_prefer_credential_token_over_ingress_file(
|
|
256
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
257
|
+
) -> None:
|
|
258
|
+
resolver = load_resolver_module()
|
|
259
|
+
now = local_now()
|
|
260
|
+
credentials_path = tmp_path / ".credentials.json"
|
|
261
|
+
future_milliseconds = int((now + timedelta(hours=1)).timestamp() * 1000)
|
|
262
|
+
write_credentials(
|
|
263
|
+
credentials_path, future_milliseconds, access_token="credential-token"
|
|
264
|
+
)
|
|
265
|
+
token_file = tmp_path / "ingress-token"
|
|
266
|
+
token_file.write_text("ingress-token-value", encoding="utf-8")
|
|
267
|
+
monkeypatch.setenv(INGRESS_TOKEN_FILE_ENV_VAR, str(token_file))
|
|
268
|
+
chosen_token = resolver.resolve_access_token(credentials_path, now)
|
|
269
|
+
assert chosen_token == "credential-token"
|
|
270
|
+
|
|
271
|
+
def should_return_none_when_ingress_file_missing(
|
|
272
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
273
|
+
) -> None:
|
|
274
|
+
resolver = load_resolver_module()
|
|
275
|
+
missing_credentials = tmp_path / "absent.json"
|
|
276
|
+
monkeypatch.setenv(INGRESS_TOKEN_FILE_ENV_VAR, str(tmp_path / "absent-token"))
|
|
277
|
+
assert resolver.resolve_access_token(missing_credentials, local_now()) is None
|
|
278
|
+
|
|
279
|
+
def should_return_none_when_ingress_file_empty(
|
|
280
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
281
|
+
) -> None:
|
|
282
|
+
resolver = load_resolver_module()
|
|
283
|
+
missing_credentials = tmp_path / "absent.json"
|
|
284
|
+
token_file = tmp_path / "ingress-token"
|
|
285
|
+
token_file.write_text(" \n", encoding="utf-8")
|
|
286
|
+
monkeypatch.setenv(INGRESS_TOKEN_FILE_ENV_VAR, str(token_file))
|
|
287
|
+
assert resolver.resolve_access_token(missing_credentials, local_now()) is None
|
|
288
|
+
|
|
289
|
+
def should_return_none_when_env_unset_and_no_credential_file(
|
|
290
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
291
|
+
) -> None:
|
|
292
|
+
resolver = load_resolver_module()
|
|
293
|
+
monkeypatch.delenv(INGRESS_TOKEN_FILE_ENV_VAR, raising=False)
|
|
294
|
+
missing_credentials = tmp_path / "absent.json"
|
|
295
|
+
assert resolver.resolve_access_token(missing_credentials, local_now()) is None
|
|
168
296
|
|
|
169
297
|
|
|
170
298
|
class TestExtractUsageWindows:
|
|
@@ -232,12 +360,20 @@ class TestBuildPausePlan:
|
|
|
232
360
|
|
|
233
361
|
|
|
234
362
|
class TestCommandLine:
|
|
235
|
-
def run_resolver(
|
|
363
|
+
def run_resolver(
|
|
364
|
+
self, *arguments: str, ingress_token_file: str | None = None
|
|
365
|
+
) -> subprocess.CompletedProcess[str]:
|
|
366
|
+
child_environment = dict(os.environ)
|
|
367
|
+
if ingress_token_file is None:
|
|
368
|
+
child_environment.pop(INGRESS_TOKEN_FILE_ENV_VAR, None)
|
|
369
|
+
else:
|
|
370
|
+
child_environment[INGRESS_TOKEN_FILE_ENV_VAR] = ingress_token_file
|
|
236
371
|
return subprocess.run(
|
|
237
372
|
[sys.executable, str(RESOLVER_PATH), *arguments],
|
|
238
373
|
capture_output=True,
|
|
239
374
|
text=True,
|
|
240
375
|
check=False,
|
|
376
|
+
env=child_environment,
|
|
241
377
|
)
|
|
242
378
|
|
|
243
379
|
def should_resolve_duration_override_end_to_end(self) -> None:
|
|
@@ -264,6 +400,38 @@ class TestCommandLine:
|
|
|
264
400
|
assert completed.returncode == 2
|
|
265
401
|
resolved_payload = json.loads(completed.stdout)
|
|
266
402
|
assert "error" in resolved_payload
|
|
403
|
+
assert str(missing_path) in resolved_payload["error"]
|
|
404
|
+
assert f"{INGRESS_TOKEN_FILE_ENV_VAR} unset" in resolved_payload["error"]
|
|
405
|
+
|
|
406
|
+
def should_name_the_ingress_token_path_in_the_error(
|
|
407
|
+
self, tmp_path: Path
|
|
408
|
+
) -> None:
|
|
409
|
+
missing_credentials = tmp_path / "absent.json"
|
|
410
|
+
stale_ingress = tmp_path / "stale-ingress-token"
|
|
411
|
+
completed = self.run_resolver(
|
|
412
|
+
"--credentials-path",
|
|
413
|
+
str(missing_credentials),
|
|
414
|
+
ingress_token_file=str(stale_ingress),
|
|
415
|
+
)
|
|
416
|
+
assert completed.returncode == 2
|
|
417
|
+
resolved_payload = json.loads(completed.stdout)
|
|
418
|
+
assert str(stale_ingress) in resolved_payload["error"]
|
|
419
|
+
|
|
420
|
+
def should_report_ingress_env_var_set_but_empty_in_the_error(
|
|
421
|
+
self, tmp_path: Path
|
|
422
|
+
) -> None:
|
|
423
|
+
missing_credentials = tmp_path / "absent.json"
|
|
424
|
+
completed = self.run_resolver(
|
|
425
|
+
"--credentials-path",
|
|
426
|
+
str(missing_credentials),
|
|
427
|
+
ingress_token_file="",
|
|
428
|
+
)
|
|
429
|
+
assert completed.returncode == 2
|
|
430
|
+
resolved_payload = json.loads(completed.stdout)
|
|
431
|
+
assert (
|
|
432
|
+
f"{INGRESS_TOKEN_FILE_ENV_VAR} set but empty"
|
|
433
|
+
in resolved_payload["error"]
|
|
434
|
+
)
|
|
267
435
|
|
|
268
436
|
def should_reject_invalid_override_with_error_payload(self) -> None:
|
|
269
437
|
completed = self.run_resolver("--override", "soon")
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""Constants for the usage-window resolver.
|
|
2
2
|
|
|
3
3
|
Groups: the OAuth usage-endpoint probe, the CLI credential file keys, the
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
and the exit codes.
|
|
4
|
+
session ingress token file environment variable, the usage-response field
|
|
5
|
+
keys, the override parse patterns, the wakeup stage sizing, the weekly warn
|
|
6
|
+
threshold, the result JSON keys, the source labels, and the exit codes.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
@@ -21,6 +21,7 @@ ALL_CREDENTIALS_RELATIVE_PATH_PARTS = (".claude", ".credentials.json")
|
|
|
21
21
|
CREDENTIALS_OAUTH_SECTION_KEY = "claudeAiOauth"
|
|
22
22
|
CREDENTIALS_ACCESS_TOKEN_KEY = "accessToken"
|
|
23
23
|
CREDENTIALS_EXPIRES_AT_KEY = "expiresAt"
|
|
24
|
+
SESSION_INGRESS_TOKEN_FILE_ENV_VAR = "CLAUDE_SESSION_INGRESS_TOKEN_FILE"
|
|
24
25
|
MILLISECONDS_PER_SECOND = 1000
|
|
25
26
|
|
|
26
27
|
FIVE_HOUR_BUCKET_KEY = "five_hour"
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: verified-build
|
|
3
3
|
description: >-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
the verifier_verdict_minter hook and unlocks the verified_commit_gate for
|
|
9
|
-
git commit/push. Use for feature implementations, refactors, and bug
|
|
10
|
-
fixes that land behind verification. Triggers: 'verified build', 'run
|
|
11
|
-
this verified', 'two-phase build', 'build and verify', 'verified
|
|
12
|
-
implementation'.
|
|
4
|
+
Two-phase verified workflow: scoped coder agents write the changes, then a
|
|
5
|
+
fresh-context code-verifier re-runs every check. Use for implementations,
|
|
6
|
+
refactors, and bug fixes. Triggers: 'verified build', 'run this verified',
|
|
7
|
+
'two-phase build', 'build and verify', 'verified implementation'.
|
|
13
8
|
---
|
|
14
9
|
|
|
15
10
|
# Verified Build
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: team-advisor-refresh
|
|
3
|
-
description: >-
|
|
4
|
-
Fired by the /team-advisor loop reminder about every 20 minutes to
|
|
5
|
-
re-assert the advisor discipline mid-run. A compressed restatement of
|
|
6
|
-
/team-advisor: orchestrate rather than execute, answer a blocked executor
|
|
7
|
-
with a plan, correction, or stop, and reuse warm agents before spawning new
|
|
8
|
-
ones. Triggers: '/team-advisor-refresh'.
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# Team Advisor Refresh
|
|
12
|
-
|
|
13
|
-
1. **You are the advisor.** Orchestrate and hold the user conversation; spawn
|
|
14
|
-
executor subagents to do all the work — every code edit and build or test
|
|
15
|
-
run.
|
|
16
|
-
2. **An executor blocked twice on the same thing?** Answer it with one signal
|
|
17
|
-
— a plan, a correction, or a stop — brief. Never take over the edit or the
|
|
18
|
-
tests yourself.
|
|
19
|
-
3. **Resume before you spawn.** `SendMessage` an existing agent by name or
|
|
20
|
-
`agentId` to reuse its warm context; prefer that over a cold spawn.
|
|
21
|
-
4. **Fresh spawn only for a genuine task switch.** No tool compacts or clears a
|
|
22
|
-
subagent's context, so a clean context comes from a fresh spawn — never tell
|
|
23
|
-
an agent to compact.
|
|
24
|
-
5. **Re-schedule the next refresh** (about 1200 seconds out) when the loop
|
|
25
|
-
mechanism needs each firing to queue the following one.
|