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.
Files changed (216) hide show
  1. package/CLAUDE.md +5 -39
  2. package/_shared/CLAUDE.md +2 -1
  3. package/_shared/advisor/CLAUDE.md +21 -0
  4. package/_shared/advisor/advisor-protocol.md +142 -0
  5. package/_shared/advisor/scripts/config/advisor_scripts_constants/__init__.py +1 -0
  6. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +86 -0
  7. package/_shared/advisor/scripts/model_tier_run_validator.py +246 -0
  8. package/_shared/advisor/scripts/pyproject.toml +3 -0
  9. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +302 -0
  10. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +180 -0
  11. package/_shared/advisor/scripts/tier_model_ids.py +164 -0
  12. package/_shared/pr-loop/CLAUDE.md +1 -0
  13. package/_shared/pr-loop/precatch-rubric.md +65 -0
  14. package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
  15. package/_shared/pr-loop/scripts/code_rules_gate.py +137 -5
  16. package/_shared/pr-loop/scripts/post_audit_thread.py +1 -1
  17. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
  18. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +16 -0
  19. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +2 -1
  20. package/_shared/pr-loop/scripts/reviewer_availability.py +42 -12
  21. package/_shared/pr-loop/scripts/reviews_disabled.py +65 -21
  22. package/_shared/pr-loop/scripts/terminology_sweep.py +69 -21
  23. package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -1
  24. package/_shared/pr-loop/scripts/tests/fixtures/{copilot_internal_user_jonecho.json → copilot_internal_user_example.json} +1 -1
  25. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +1 -1
  26. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +146 -2
  27. package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +8 -8
  28. package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +18 -5
  29. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +56 -5
  30. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +104 -17
  31. package/agents/CLAUDE.md +3 -2
  32. package/agents/clasp-deployment-orchestrator.md +2 -2
  33. package/agents/code-advisor.md +7 -5
  34. package/agents/code-verifier.md +6 -2
  35. package/agents/session-advisor.md +27 -0
  36. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
  37. package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -1
  38. package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
  39. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
  40. package/audit-rubrics/prompts/category-c-resource-cleanup.md +2 -2
  41. package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +2 -2
  42. package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
  43. package/audit-rubrics/prompts/category-f-silent-failures.md +2 -2
  44. package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
  45. package/audit-rubrics/prompts/category-h-security-boundaries.md +2 -2
  46. package/audit-rubrics/prompts/category-i-concurrency.md +2 -2
  47. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +2 -2
  48. package/audit-rubrics/prompts/category-k-codebase-conflicts.md +2 -2
  49. package/audit-rubrics/prompts/category-l-behavior-equivalence.md +2 -2
  50. package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +2 -2
  51. package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +2 -2
  52. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
  53. package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +2 -2
  54. package/bin/CLAUDE.md +1 -0
  55. package/bin/expand_home_directory_tokens.mjs +75 -0
  56. package/bin/install.mjs +21 -7
  57. package/bin/install.test.mjs +220 -10
  58. package/hooks/CLAUDE.md +1 -1
  59. package/hooks/blocking/CLAUDE.md +4 -1
  60. package/hooks/blocking/_path_setup.py +13 -0
  61. package/hooks/blocking/bash_pre_tool_use_dispatcher.py +281 -0
  62. package/hooks/blocking/code_rules_dead_module_constant.py +88 -16
  63. package/hooks/blocking/code_rules_enforcer.py +6 -0
  64. package/hooks/blocking/code_rules_imports_logging.py +34 -1
  65. package/hooks/blocking/code_rules_paths_syspath.py +1 -1
  66. package/hooks/blocking/code_rules_test_layout.py +272 -0
  67. package/hooks/blocking/nas_ssh_binary_enforcer.py +22 -12
  68. package/hooks/blocking/pii_prevention_blocker.py +781 -0
  69. package/hooks/blocking/pii_scanner.py +340 -0
  70. package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
  71. package/hooks/blocking/stop_dispatcher.py +84 -0
  72. package/hooks/blocking/test__path_setup.py +39 -0
  73. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +61 -0
  74. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +294 -0
  75. package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +1 -1
  76. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +51 -0
  77. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +103 -0
  78. package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +1 -1
  79. package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +1 -1
  80. package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +11 -11
  81. package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +1 -1
  82. package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +1 -1
  83. package/hooks/blocking/test_code_rules_enforcer_test_layout.py +111 -0
  84. package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +76 -0
  85. package/hooks/blocking/test_destructive_command_blocker.py +29 -9
  86. package/hooks/blocking/test_gh_body_arg_blocker.py +3 -3
  87. package/hooks/blocking/test_nas_ssh_binary_enforcer.py +96 -39
  88. package/hooks/blocking/test_pii_prevention_blocker.py +905 -0
  89. package/hooks/blocking/test_pii_prevention_windows_git_path.py +41 -0
  90. package/hooks/blocking/test_pii_scanner.py +165 -0
  91. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +1 -1
  92. package/hooks/blocking/test_pre_tool_use_dispatcher.py +14 -13
  93. package/hooks/blocking/test_stop_dispatcher.py +187 -0
  94. package/hooks/blocking/test_verdict_directory_write_blocker.py +1 -1
  95. package/hooks/blocking/test_volatile_path_in_post_blocker.py +3 -3
  96. package/hooks/blocking/volatile_path_in_post_blocker.py +54 -5
  97. package/hooks/diagnostic/migrations/README.md +25 -26
  98. package/hooks/diagnostic/test_hook_log_extractor.py +10 -10
  99. package/hooks/hooks.json +10 -120
  100. package/hooks/hooks_constants/CLAUDE.md +12 -3
  101. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +78 -0
  102. package/hooks/hooks_constants/dead_module_constant_constants.py +1 -0
  103. package/hooks/hooks_constants/hosted_hook_runner.py +73 -0
  104. package/hooks/hooks_constants/local_identity.py +182 -0
  105. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +4 -37
  106. package/hooks/hooks_constants/pii_prevention_constants.py +299 -0
  107. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  108. package/hooks/hooks_constants/python_style_checks_constants.py +15 -0
  109. package/hooks/hooks_constants/stop_dispatcher_constants.py +26 -0
  110. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +87 -0
  111. package/hooks/hooks_constants/test_hardcoded_user_path_constants.py +2 -2
  112. package/hooks/hooks_constants/test_hosted_hook_runner.py +101 -0
  113. package/hooks/hooks_constants/test_layout_constants.py +28 -0
  114. package/hooks/hooks_constants/test_local_identity.py +88 -0
  115. package/hooks/hooks_constants/test_session_env_cleanup_constants.py +1 -1
  116. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +32 -0
  117. package/hooks/validators/README.md +5 -6
  118. package/hooks/validators/__init__.py +2 -2
  119. package/hooks/validators/python_style_checks.py +298 -243
  120. package/hooks/validators/run_all_validators.py +6 -0
  121. package/hooks/validators/test_python_style_checks.py +278 -163
  122. package/package.json +2 -2
  123. package/rules/CLAUDE.md +7 -1
  124. package/rules/bdd.md +1 -1
  125. package/rules/nas-ssh-invocation.md +6 -4
  126. package/rules/no-justification-noise.md +61 -0
  127. package/rules/testing.md +0 -2
  128. package/scripts/test_setup_project_paths.py +1 -1
  129. package/skills/CLAUDE.md +6 -3
  130. package/skills/_shared/pr-loop/scripts/_path_resolver.py +2 -2
  131. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
  132. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
  133. package/skills/_shared/pr-loop/scripts/test__path_resolver.py +42 -3
  134. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +11 -11
  135. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +26 -3
  136. package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +49 -25
  137. package/skills/autoconverge/CLAUDE.md +3 -3
  138. package/skills/autoconverge/SKILL.md +113 -35
  139. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  140. package/skills/autoconverge/reference/convergence.md +44 -22
  141. package/skills/autoconverge/reference/gotchas.md +11 -0
  142. package/skills/autoconverge/reference/stop-conditions.md +23 -13
  143. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  144. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +15 -19
  145. package/skills/autoconverge/workflow/converge.contract.test.mjs +43 -42
  146. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +141 -16
  147. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +2 -2
  148. package/skills/autoconverge/workflow/converge.mjs +329 -68
  149. package/skills/autoconverge/workflow/converge.path-aware.test.mjs +1 -1
  150. package/skills/autoconverge/workflow/converge.precatch.test.mjs +152 -0
  151. package/skills/autoconverge/workflow/converge.run-input.test.mjs +3 -3
  152. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +15 -15
  153. package/skills/autoconverge/workflow/test_convergence_summary.py +6 -6
  154. package/skills/bdd-protocol/SKILL.md +4 -5
  155. package/skills/bdd-protocol/references/anti-patterns.md +1 -1
  156. package/skills/bugteam/SKILL.md +8 -0
  157. package/skills/bugteam/reference/copilot-gap-analysis.md +20 -20
  158. package/skills/bugteam/reference/obstacles/audit-walk-categories.md +1 -1
  159. package/skills/bugteam/reference/team-setup.md +7 -5
  160. package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
  161. package/skills/copilot-finding-triage/SKILL.md +124 -0
  162. package/skills/copilot-finding-triage/reference/tier-rubric.md +49 -0
  163. package/skills/copilot-finding-triage/scripts/conftest.py +8 -0
  164. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/__init__.py +0 -0
  165. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/__init__.py +0 -0
  166. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/notify_ntfy_constants.py +32 -0
  167. package/skills/copilot-finding-triage/scripts/notify_ntfy.py +192 -0
  168. package/skills/copilot-finding-triage/scripts/test_notify_ntfy.py +141 -0
  169. package/skills/copilot-finding-triage/templates/notification.md +34 -0
  170. package/skills/copilot-review/SKILL.md +8 -6
  171. package/skills/findbugs/SKILL.md +4 -0
  172. package/skills/fixbugs/SKILL.md +8 -7
  173. package/skills/gotcha/CLAUDE.md +2 -2
  174. package/skills/gotcha/SKILL.md +4 -4
  175. package/skills/log-audit/SKILL.md +4 -6
  176. package/skills/monitor-open-prs/CLAUDE.md +1 -1
  177. package/skills/monitor-open-prs/SKILL.md +6 -1
  178. package/skills/orchestrator/SKILL.md +184 -0
  179. package/skills/orchestrator-refresh/SKILL.md +37 -0
  180. package/skills/post-audit-findings/SKILL.md +5 -9
  181. package/skills/pr-consistency-audit/SKILL.md +5 -1
  182. package/skills/pr-converge/CLAUDE.md +1 -1
  183. package/skills/pr-converge/SKILL.md +86 -47
  184. package/skills/pr-converge/reference/CLAUDE.md +1 -1
  185. package/skills/pr-converge/reference/convergence-gates.md +20 -17
  186. package/skills/pr-converge/reference/examples.md +63 -47
  187. package/skills/pr-converge/reference/fix-protocol.md +15 -15
  188. package/skills/pr-converge/reference/ground-rules.md +11 -7
  189. package/skills/pr-converge/reference/multi-pr-orchestration.md +6 -5
  190. package/skills/pr-converge/reference/per-tick.md +129 -107
  191. package/skills/pr-converge/reference/state-schema.md +15 -10
  192. package/skills/pr-converge/scripts/check_convergence.py +5 -4
  193. package/skills/pr-converge/scripts/test_check_convergence.py +6 -4
  194. package/skills/pr-fix-protocol/SKILL.md +3 -8
  195. package/skills/pr-loop-cloud-transport/SKILL.md +102 -0
  196. package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +40 -0
  197. package/skills/pr-loop-cloud-transport/reference/substitution-matrix.md +48 -0
  198. package/skills/pr-loop-lifecycle/SKILL.md +5 -9
  199. package/skills/pr-scope-resolve/SKILL.md +3 -6
  200. package/skills/privacy-hygiene/SKILL.md +114 -0
  201. package/skills/qbug/SKILL.md +8 -8
  202. package/skills/rebase/SKILL.md +5 -1
  203. package/skills/refine/SKILL.md +4 -5
  204. package/skills/reviewer-gates/SKILL.md +7 -11
  205. package/skills/session-log/SKILL.md +4 -1
  206. package/skills/skill-builder/SKILL.md +3 -6
  207. package/skills/structure-prompt/SKILL.md +4 -5
  208. package/skills/team-advisor/SKILL.md +32 -164
  209. package/skills/test_markdown_link_integrity.py +10 -6
  210. package/skills/update/SKILL.md +5 -1
  211. package/skills/usage-pause/SKILL.md +14 -5
  212. package/skills/usage-pause/scripts/resolve_usage_window.py +83 -5
  213. package/skills/usage-pause/scripts/test_resolve_usage_window.py +185 -17
  214. package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +4 -3
  215. package/skills/verified-build/SKILL.md +4 -9
  216. 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 reads the Claude Code OAuth access token
11
- from the CLI credential file and asks the OAuth usage endpoint for the
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 = read_oauth_access_token(credentials_path, now)
504
+ access_token = resolve_access_token(credentials_path, now)
428
505
  if access_token is None:
429
506
  return _emit_error(
430
- "OAuth access token unavailable or expired; give a manual reset time, "
431
- "for example /usage-pause 10:20pm or /usage-pause 74m"
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
- self.write_credentials(credentials_path, future_milliseconds)
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
- self.write_credentials(credentials_path, past_milliseconds)
171
+ write_credentials(credentials_path, past_milliseconds)
162
172
  assert resolver.read_oauth_access_token(credentials_path, now) is None
163
173
 
164
- def should_return_none_when_file_missing(self, tmp_path: Path) -> None:
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
- assert resolver.read_oauth_access_token(missing_path, local_now()) is None
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(self, *arguments: str) -> subprocess.CompletedProcess[str]:
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
- 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.
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
- Runs a code task through the two-phase verified workflow: scoped coder
5
- agents write the changes (consulting the tool-less code-advisor when
6
- stuck on a decision), then a fresh-context code-verifier agent re-derives
7
- and runs every check itself. The verifier's fenced verdict is minted by
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.