claude-dev-env 8.12.0 → 8.13.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 (74) hide show
  1. package/.agents/skills/pull-request/scripts/test_pull_request.py +1 -1
  2. package/.agents/skills-archived/pr-converge/scripts/test_check_convergence.py +4 -4
  3. package/_shared/advisor/reference/astra-rung.md +7 -7
  4. package/_shared/advisor/reference/third-party-bind.md +2 -2
  5. package/_shared/advisor/scripts/codex_astra_advisor.py +33 -26
  6. package/_shared/advisor/scripts/codex_astra_preflight.py +57 -55
  7. package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +8 -8
  8. package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +69 -23
  9. package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +4 -4
  10. package/_shared/pr-loop/scripts/check_convergence.py +54 -49
  11. package/_shared/pr-loop/scripts/codex_review_scripts_constants/run_constants.py +4 -0
  12. package/_shared/pr-loop/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +10 -1
  13. package/_shared/pr-loop/scripts/run_codex_review.py +4 -6
  14. package/_shared/pr-loop/scripts/test_check_convergence.py +6 -6
  15. package/_shared/pr-loop/scripts/test_check_convergence_codex.py +73 -31
  16. package/_shared/pr-loop/scripts/test_check_convergence_contract.py +2 -2
  17. package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +2 -2
  18. package/bin/git_hooks_installer.test.mjs +1 -1
  19. package/bin/install-constants.mjs +1 -6
  20. package/bin/install.cursor-rules.test.mjs +24 -51
  21. package/bin/install.mjs +16 -70
  22. package/docs/codex-accounts.md +79 -0
  23. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +5 -5
  24. package/hooks/blocking/test_pii_scanner.py +1 -1
  25. package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +2 -2
  26. package/hooks/git-hooks/conftest.py +16 -0
  27. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +1 -1
  28. package/hooks/git-hooks/test_post_commit.py +1 -1
  29. package/hooks/git-hooks/test_pre_commit.py +1 -1
  30. package/hooks/git-hooks/test_pre_push.py +2 -2
  31. package/hooks/git-hooks/test_verification_start.py +1 -1
  32. package/hooks/git-hooks/verification_notice.py +2 -2
  33. package/hooks/git-hooks/verification_notice_context.py +21 -2
  34. package/hooks/git-hooks/verification_start.py +2 -3
  35. package/package.json +1 -1
  36. package/rules/durable-post-artifacts.md +4 -3
  37. package/scripts/claude_account_profile.py +21 -9
  38. package/scripts/codex_account_choice.py +396 -0
  39. package/scripts/codex_account_meters.py +372 -0
  40. package/scripts/dev_env_scripts_constants/codex_account_constants.py +141 -0
  41. package/scripts/dev_env_scripts_constants/private_term_constants.py +17 -2
  42. package/scripts/private_term_scan.py +6 -2
  43. package/scripts/repository_checks/config/constants.py +1 -0
  44. package/scripts/repository_checks/tracked_private_terms.py +7 -0
  45. package/scripts/test_claude_account_profile.py +12 -0
  46. package/scripts/test_codex_account_choice.py +276 -0
  47. package/scripts/test_codex_account_meters.py +205 -0
  48. package/scripts/tests/test_automatic_advisory.py +3 -3
  49. package/scripts/tests/test_automatic_advisory_polling.py +1 -1
  50. package/scripts/tests/test_automatic_advisory_recovery.py +1 -1
  51. package/scripts/tests/test_closed_pr_label.py +1 -1
  52. package/scripts/tests/test_private_term_scan.py +28 -0
  53. package/scripts/tests/test_review_closure.py +2 -2
  54. package/scripts/tests/test_review_closure_model.py +2 -2
  55. package/scripts/tests/test_rules.py +2 -83
  56. package/scripts/tests/test_tracked_private_terms.py +15 -0
  57. package/scripts/tests/test_verification_notice.py +29 -13
  58. package/.agents/skills/cert-classification-rule/SKILL.md +0 -224
  59. package/.agents/skills/cert-classification-rule/scripts/resort_cert_folders.py +0 -158
  60. package/_shared/pr-loop/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -82
  61. package/_shared/pr-loop/scripts/codex_usage_probe.py +0 -573
  62. package/_shared/pr-loop/scripts/test_codex_usage_probe.py +0 -683
  63. package/scripts/sync_to_cursor/AGENTS.md +0 -1
  64. package/scripts/sync_to_cursor/CLAUDE.md +0 -1
  65. package/scripts/sync_to_cursor/__init__.py +0 -13
  66. package/scripts/sync_to_cursor/canonical_docs.py +0 -66
  67. package/scripts/sync_to_cursor/config/__init__.py +0 -8
  68. package/scripts/sync_to_cursor/engine.py +0 -219
  69. package/scripts/sync_to_cursor/hashing.py +0 -7
  70. package/scripts/sync_to_cursor/paths.py +0 -18
  71. package/scripts/sync_to_cursor/rules.py +0 -494
  72. package/scripts/sync_to_cursor.py +0 -22
  73. package/scripts/tests/test_engine.py +0 -101
  74. package/scripts/tests/test_sync_to_cursor.py +0 -315
@@ -29,9 +29,10 @@ The frozen process env is only a fallback when that disk read fails (logged once
29
29
  A probe error does not waive the gate; the live GitHub checks still run.
30
30
 
31
31
  The Codex gate is conditional-required: it demands
32
- ``codex_clean_at == current_head`` only when the weekly usage probe reports
33
- more than the probe's threshold percent left. At or below that threshold, null
34
- usage, the codex token, or ``codex_down`` never blocks ready.
32
+ ``codex_clean_at == current_head`` only when the Codex account picker
33
+ (``scripts/codex_account_choice.py choose``) answers the ``normal`` tier, so
34
+ one signed-in account has room for a review. A ``luna`` or ``wait`` answer, an
35
+ unreadable picker, the codex token, or ``codex_down`` never blocks ready.
35
36
  """
36
37
 
37
38
  from __future__ import annotations
@@ -62,13 +63,12 @@ from check_convergence_thread_gates import (
62
63
  _check_no_pending_reviews,
63
64
  _count_unresolved_bot_threads,
64
65
  )
65
- from codex_review_scripts_constants.codex_usage_probe_constants import (
66
- USAGE_REPORT_KEY_PERCENT_LEFT,
67
- )
68
- from codex_usage_probe import is_codex_review_required, probe_weekly_usage_via_subprocess
69
66
  from pr_converge_scripts_constants.convergence_gate_constants import (
67
+ ALL_CODEX_ACCOUNT_PICKER_RELATIVE_PARTS,
70
68
  BUGBOT_DOWN_BYPASS_NOTE,
71
69
  CLAUDE_JOB_DIR_ENV_VAR_NAME,
70
+ CODEX_ACCOUNT_PICK_TIMEOUT_SECONDS,
71
+ CODEX_ACCOUNT_PICKER_CHOOSE_COMMAND,
72
72
  CODEX_BYPASS_DETAIL,
73
73
  CODEX_CLEAN_AT_STATE_KEY,
74
74
  CODEX_CLEAN_DETAIL_TEMPLATE,
@@ -76,11 +76,13 @@ from pr_converge_scripts_constants.convergence_gate_constants import (
76
76
  CODEX_GATE_LABEL,
77
77
  CODEX_MISSING_CLEAN_DETAIL_TEMPLATE,
78
78
  CODEX_SKIPPED_USAGE_DETAIL,
79
+ CODEX_TIER_KEY,
80
+ CODEX_TIER_NORMAL,
79
81
  COPILOT_DOWN_BYPASS_NOTE,
80
82
  FIXTURE_DEFAULT_PENDING_DETAIL,
81
83
  FIXTURE_DEFAULT_THREADS_DETAIL,
82
84
  FIXTURE_KEY_CODEX_CLEAN_AT,
83
- FIXTURE_KEY_CODEX_PERCENT_LEFT,
85
+ FIXTURE_KEY_CODEX_TIER,
84
86
  FIXTURE_KEY_HEAD_SHA,
85
87
  FIXTURE_KEY_PENDING_REVIEWS_DETAIL,
86
88
  FIXTURE_KEY_PENDING_REVIEWS_PASSED,
@@ -90,6 +92,7 @@ from pr_converge_scripts_constants.convergence_gate_constants import (
90
92
  FIXTURE_KEY_UNRESOLVED_BOT_THREADS_PASSED,
91
93
  MINIMUM_ABBREVIATED_SHA_LENGTH,
92
94
  PR_CONVERGE_STATE_FILENAME,
95
+ SHARED_PACKAGE_ROOT_PARENT_INDEX,
93
96
  )
94
97
  from pr_converge_skill_constants.constants import (
95
98
  ALL_COPILOT_CLEAN_REVIEW_STATES,
@@ -128,7 +131,7 @@ class ConvergenceFixture(NamedTuple):
128
131
  unresolved_bot_threads_detail: str
129
132
  is_no_pending_reviews: bool
130
133
  pending_reviews_detail: str
131
- codex_percent_left: float | None
134
+ codex_tier: str | None
132
135
  codex_clean_at: str | None
133
136
 
134
137
 
@@ -333,24 +336,24 @@ def _sha_matches_head(stamp_sha: str, head_sha: str) -> bool:
333
336
 
334
337
  def _evaluate_codex_clean(
335
338
  *,
336
- read_percent_left: Callable[[], float | None],
339
+ read_codex_tier: Callable[[], str | None],
337
340
  codex_clean_at: str | None,
338
341
  head_sha: str,
339
342
  ) -> tuple[bool, str]:
340
- """Return whether the conditional Codex gate passes for the given stamp and usage.
343
+ """Return whether the conditional Codex gate passes for the given stamp and account tier.
341
344
 
342
345
  ::
343
346
 
344
- clean stamp on head -> pass (usage never read)
345
- no stamp, percent None or <= limit -> skip (never blocks)
346
- no stamp, percent > limit -> fail
347
+ clean stamp on head -> pass (picker never runs)
348
+ no stamp, tier luna, wait, or None -> skip (never blocks)
349
+ no stamp, tier normal -> fail
347
350
 
348
- A stamp on the current HEAD settles the gate on its own, so the usage read
349
- stays behind it: the weekly probe spawns a Codex subprocess, and the common
350
- already-clean tick has no reason to pay for one.
351
+ A stamp on the current HEAD settles the gate on its own, so the picker
352
+ stays behind it: the picker starts one Codex app server per account, and
353
+ the common already-clean tick has no reason to pay for them.
351
354
 
352
355
  Args:
353
- read_percent_left: Reads weekly Codex percent remaining, or None when unknown.
356
+ read_codex_tier: Returns the picker's tier, or None when the picker cannot say.
354
357
  codex_clean_at: HEAD SHA where Codex last reported clean, or None.
355
358
  head_sha: Current PR HEAD commit SHA.
356
359
 
@@ -359,32 +362,38 @@ def _evaluate_codex_clean(
359
362
  """
360
363
  if codex_clean_at is not None and _sha_matches_head(codex_clean_at, head_sha):
361
364
  return True, CODEX_CLEAN_DETAIL_TEMPLATE % _short_sha(head_sha)
362
- if not is_codex_review_required(read_percent_left()):
365
+ if read_codex_tier() != CODEX_TIER_NORMAL:
363
366
  return True, CODEX_SKIPPED_USAGE_DETAIL
364
367
  return False, CODEX_MISSING_CLEAN_DETAIL_TEMPLATE % _short_sha(head_sha)
365
368
 
366
369
 
367
- def _probe_codex_percent_left() -> float | None:
368
- """Probe weekly Codex usage and return percent remaining, or None when unknown."""
370
+ def _codex_account_picker_path() -> Path:
371
+ """Return the installed Codex account picker beside this shared tree."""
372
+ shared_root = Path(__file__).resolve().parents[SHARED_PACKAGE_ROOT_PARENT_INDEX]
373
+ return shared_root.joinpath(*ALL_CODEX_ACCOUNT_PICKER_RELATIVE_PARTS)
374
+
375
+
376
+ def _read_codex_tier() -> str | None:
377
+ """Ask the Codex account picker which tier a review runs on, or None when it cannot say."""
369
378
  try:
370
- usage_report = probe_weekly_usage_via_subprocess()
371
- except (
372
- FileNotFoundError,
373
- OSError,
374
- subprocess.TimeoutExpired,
375
- subprocess.SubprocessError,
376
- json.JSONDecodeError,
377
- ValueError,
378
- TypeError,
379
- KeyError,
380
- ):
379
+ completed = subprocess.run(
380
+ [
381
+ sys.executable,
382
+ str(_codex_account_picker_path()),
383
+ CODEX_ACCOUNT_PICKER_CHOOSE_COMMAND,
384
+ ],
385
+ capture_output=True,
386
+ text=True,
387
+ check=False,
388
+ timeout=CODEX_ACCOUNT_PICK_TIMEOUT_SECONDS,
389
+ )
390
+ answer = json.loads(completed.stdout)
391
+ except (OSError, subprocess.SubprocessError, json.JSONDecodeError):
381
392
  return None
382
- raw_percent = usage_report.get(USAGE_REPORT_KEY_PERCENT_LEFT)
383
- if isinstance(raw_percent, bool):
393
+ if completed.returncode != 0 or not isinstance(answer, dict):
384
394
  return None
385
- if isinstance(raw_percent, (int, float)):
386
- return float(raw_percent)
387
- return None
395
+ tier = answer.get(CODEX_TIER_KEY)
396
+ return tier if isinstance(tier, str) else None
388
397
 
389
398
 
390
399
  def _read_job_state() -> dict[str, object]:
@@ -468,11 +477,11 @@ def _codex_condition(context: GateContext) -> GateCondition:
468
477
  if context.is_codex_down:
469
478
  return (CODEX_GATE_LABEL, (True, CODEX_BYPASS_DETAIL))
470
479
  if context.fixture is not None:
471
- fixture_percent_left = context.fixture.codex_percent_left
480
+ fixture_codex_tier = context.fixture.codex_tier
472
481
  return (
473
482
  CODEX_GATE_LABEL,
474
483
  _evaluate_codex_clean(
475
- read_percent_left=lambda: fixture_percent_left,
484
+ read_codex_tier=lambda: fixture_codex_tier,
476
485
  codex_clean_at=context.fixture.codex_clean_at,
477
486
  head_sha=context.head_sha,
478
487
  ),
@@ -480,7 +489,7 @@ def _codex_condition(context: GateContext) -> GateCondition:
480
489
  return (
481
490
  CODEX_GATE_LABEL,
482
491
  _evaluate_codex_clean(
483
- read_percent_left=_probe_codex_percent_left,
492
+ read_codex_tier=_read_codex_tier,
484
493
  codex_clean_at=context.live_codex_clean_at,
485
494
  head_sha=context.head_sha,
486
495
  ),
@@ -623,17 +632,13 @@ def _load_convergence_fixture(from_path: Path) -> ConvergenceFixture:
623
632
  raise ValueError("fixture pending_reviews_passed must be a bool")
624
633
  if not isinstance(pending_detail, str):
625
634
  raise ValueError("fixture pending_reviews_detail must be a string")
626
- codex_percent_left = payload.get(FIXTURE_KEY_CODEX_PERCENT_LEFT)
635
+ codex_tier = payload.get(FIXTURE_KEY_CODEX_TIER)
627
636
  codex_clean_at = payload.get(FIXTURE_KEY_CODEX_CLEAN_AT)
628
- if isinstance(codex_percent_left, bool):
629
- raise ValueError("fixture codex_percent_left must be a number or null")
630
- if codex_percent_left is not None and not isinstance(codex_percent_left, (int, float)):
631
- raise ValueError("fixture codex_percent_left must be a number or null")
637
+ if codex_tier is not None and not isinstance(codex_tier, str):
638
+ raise ValueError("fixture codex_tier must be a string or null")
632
639
  if codex_clean_at is not None and not isinstance(codex_clean_at, str):
633
640
  raise ValueError("fixture codex_clean_at must be a string or null")
634
- typed_percent_left: float | None = (
635
- float(codex_percent_left) if isinstance(codex_percent_left, (int, float)) else None
636
- )
641
+ typed_codex_tier: str | None = codex_tier if isinstance(codex_tier, str) else None
637
642
  typed_codex_clean_at: str | None = codex_clean_at if isinstance(codex_clean_at, str) else None
638
643
  return ConvergenceFixture(
639
644
  head_sha=head_sha,
@@ -643,7 +648,7 @@ def _load_convergence_fixture(from_path: Path) -> ConvergenceFixture:
643
648
  unresolved_bot_threads_detail=threads_detail,
644
649
  is_no_pending_reviews=is_pending_ok,
645
650
  pending_reviews_detail=pending_detail,
646
- codex_percent_left=typed_percent_left,
651
+ codex_tier=typed_codex_tier,
647
652
  codex_clean_at=typed_codex_clean_at,
648
653
  )
649
654
 
@@ -3,6 +3,10 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  CODEX_BINARY_NAME = "codex"
6
+ WINDOWS_OS_NAME = "nt"
7
+ WINDOWS_COMMAND_SHELL = "cmd"
8
+ WINDOWS_COMMAND_SHELL_RUN_FLAG = "/c"
9
+ ALL_WINDOWS_SCRIPT_SUFFIXES = (".cmd", ".bat")
6
10
  EXEC_SUBCOMMAND = "exec"
7
11
  REVIEW_SUBCOMMAND = "review"
8
12
  HELP_FLAG = "--help"
@@ -45,7 +45,7 @@ FIXTURE_KEY_UNRESOLVED_BOT_THREADS_PASSED: str = "unresolved_bot_threads_passed"
45
45
  FIXTURE_KEY_UNRESOLVED_BOT_THREADS_DETAIL: str = "unresolved_bot_threads_detail"
46
46
  FIXTURE_KEY_PENDING_REVIEWS_PASSED: str = "pending_reviews_passed"
47
47
  FIXTURE_KEY_PENDING_REVIEWS_DETAIL: str = "pending_reviews_detail"
48
- FIXTURE_KEY_CODEX_PERCENT_LEFT: str = "codex_percent_left"
48
+ FIXTURE_KEY_CODEX_TIER: str = "codex_tier"
49
49
  FIXTURE_KEY_CODEX_CLEAN_AT: str = "codex_clean_at"
50
50
  FIXTURE_DEFAULT_THREADS_DETAIL: str = "0 unresolved"
51
51
  FIXTURE_DEFAULT_PENDING_DETAIL: str = "none pending"
@@ -68,3 +68,12 @@ PR_CONVERGE_STATE_FILENAME: str = "pr-converge-state.json"
68
68
  CODEX_CLEAN_AT_STATE_KEY: str = "codex_clean_at"
69
69
  CODEX_DOWN_STATE_KEY: str = "codex_down"
70
70
  MINIMUM_ABBREVIATED_SHA_LENGTH: int = 7
71
+ SHARED_PACKAGE_ROOT_PARENT_INDEX: int = 3
72
+ ALL_CODEX_ACCOUNT_PICKER_RELATIVE_PARTS: tuple[str, ...] = (
73
+ "scripts",
74
+ "codex_account_choice.py",
75
+ )
76
+ CODEX_ACCOUNT_PICKER_CHOOSE_COMMAND: str = "choose"
77
+ CODEX_ACCOUNT_PICK_TIMEOUT_SECONDS: float = 150.0
78
+ CODEX_TIER_KEY: str = "tier"
79
+ CODEX_TIER_NORMAL: str = "normal"
@@ -44,14 +44,9 @@ from process_tree_kill import ( # noqa: E402
44
44
  terminate_process_tree,
45
45
  )
46
46
 
47
- from codex_review_scripts_constants.codex_usage_probe_constants import ( # noqa: E402
48
- ALL_WINDOWS_SCRIPT_SUFFIXES,
49
- WINDOWS_COMMAND_SHELL,
50
- WINDOWS_COMMAND_SHELL_RUN_FLAG,
51
- WINDOWS_OS_NAME,
52
- )
53
47
  from codex_review_scripts_constants.run_constants import ( # noqa: E402
54
48
  ALL_SHAPE_PROBE_REQUIRED_FLAGS,
49
+ ALL_WINDOWS_SCRIPT_SUFFIXES,
55
50
  BASE_TARGET_FLAG,
56
51
  CAPTURE_STREAMS_KEYWORD,
57
52
  CHECK_KEYWORD,
@@ -88,6 +83,9 @@ from codex_review_scripts_constants.run_constants import ( # noqa: E402
88
83
  UTF8_ENCODING,
89
84
  VERSION_FLAG,
90
85
  VERSION_PROBE_PATTERN,
86
+ WINDOWS_COMMAND_SHELL,
87
+ WINDOWS_COMMAND_SHELL_RUN_FLAG,
88
+ WINDOWS_OS_NAME,
91
89
  )
92
90
 
93
91
  from subprocess_window_access import hidden_window_creation_flags
@@ -107,14 +107,14 @@ def _raise_if_called(gate_label: str) -> Callable[..., tuple[bool, str]]:
107
107
  return _stub
108
108
 
109
109
 
110
- def _null_codex_percent_left() -> float | None:
110
+ def _unread_codex_tier() -> str | None:
111
111
  return None
112
112
 
113
113
 
114
114
  def _patch_all_gates_passing(monkeypatch: pytest.MonkeyPatch) -> None:
115
115
  monkeypatch.setattr(check_convergence, "_get_pr_head_sha", _passing_head_sha)
116
116
  monkeypatch.setattr(
117
- check_convergence, "_probe_codex_percent_left", _null_codex_percent_left
117
+ check_convergence, "_read_codex_tier", _unread_codex_tier
118
118
  )
119
119
  for each_gate_name in _ALL_LEAF_GATE_NAMES:
120
120
  monkeypatch.setattr(check_convergence, each_gate_name, _passing_gate)
@@ -135,7 +135,7 @@ def should_pass_when_clean_bugteam_review_present_on_current_head(
135
135
  check_convergence, "_gh_api_paginated", _make_stub_gh_paginated(reviews_payload)
136
136
  )
137
137
  passed, detail = check_convergence._check_bugteam_clean(
138
- owner="JonEcho", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
138
+ owner="octocat", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
139
139
  )
140
140
  assert passed is True
141
141
  assert "clean bugteam audit" in detail
@@ -157,7 +157,7 @@ def should_fail_when_dirty_bugteam_review_present_on_current_head(
157
157
  check_convergence, "_gh_api_paginated", _make_stub_gh_paginated(reviews_payload)
158
158
  )
159
159
  passed, detail = check_convergence._check_bugteam_clean(
160
- owner="JonEcho", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
160
+ owner="octocat", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
161
161
  )
162
162
  assert passed is False
163
163
  assert "dirty bugteam audit" in detail
@@ -185,7 +185,7 @@ def should_fail_when_no_bugteam_review_present_on_current_head(
185
185
  check_convergence, "_gh_api_paginated", _make_stub_gh_paginated(reviews_payload)
186
186
  )
187
187
  passed, detail = check_convergence._check_bugteam_clean(
188
- owner="JonEcho", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
188
+ owner="octocat", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
189
189
  )
190
190
  assert passed is False
191
191
  assert "no bugteam review found" in detail
@@ -204,7 +204,7 @@ def should_fail_with_shape_detail_when_gh_returns_non_list_payload(
204
204
  check_convergence, "_gh_api_paginated", stub_gh_api_paginated_returning_object
205
205
  )
206
206
  passed, detail = check_convergence._check_bugteam_clean(
207
- owner="JonEcho", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
207
+ owner="octocat", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
208
208
  )
209
209
  assert passed is False
210
210
  assert "unexpected gh api response shape" in detail
@@ -1,8 +1,8 @@
1
1
  """Fixture-driven tests for the conditional Codex convergence gate.
2
2
 
3
- Covers: required-and-clean, required-and-dirty, skipped-by-threshold,
4
- skipped-by-token, skipped-by-down. Threshold comes from the probe constant
5
- via ``is_codex_review_required`` — this module never inlines the percent.
3
+ Covers: required-and-clean, required-and-dirty, skipped-by-tier,
4
+ skipped-by-token, skipped-by-down, and the read of the Codex account picker's
5
+ ``choose`` answer. The required tier comes from ``CODEX_TIER_NORMAL``.
6
6
  """
7
7
 
8
8
  from __future__ import annotations
@@ -16,10 +16,8 @@ from types import ModuleType
16
16
  import pytest
17
17
 
18
18
  import _pr_converge_path_setup # noqa: F401
19
- from codex_review_scripts_constants.codex_usage_probe_constants import (
20
- WEEKLY_USAGE_GATE_THRESHOLD_PERCENT,
21
- )
22
19
  from pr_converge_scripts_constants.convergence_gate_constants import (
20
+ CODEX_TIER_NORMAL,
23
21
  MINIMUM_ABBREVIATED_SHA_LENGTH,
24
22
  )
25
23
 
@@ -33,8 +31,7 @@ CLEAN_BODY = (
33
31
  )
34
32
  HEAD_SHA = "ae8005aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
35
33
  OTHER_SHA = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
36
- PERCENT_ABOVE_THRESHOLD = float(WEEKLY_USAGE_GATE_THRESHOLD_PERCENT) + 1.0
37
- PERCENT_AT_THRESHOLD = float(WEEKLY_USAGE_GATE_THRESHOLD_PERCENT)
34
+ ALL_TIERS_WITHOUT_ROOM_FOR_REVIEW = ("luna", "wait")
38
35
 
39
36
 
40
37
  def _load_module() -> ModuleType:
@@ -64,7 +61,7 @@ check_convergence = _load_module()
64
61
  def _write_fixture(
65
62
  tmp_path: Path,
66
63
  *,
67
- codex_percent_left: float | None,
64
+ codex_tier: str | None,
68
65
  codex_clean_at: str | None,
69
66
  filename: str = "codex-gate.json",
70
67
  ) -> Path:
@@ -83,7 +80,7 @@ def _write_fixture(
83
80
  "unresolved_bot_threads_detail": "0 unresolved",
84
81
  "pending_reviews_passed": True,
85
82
  "pending_reviews_detail": "none pending",
86
- "codex_percent_left": codex_percent_left,
83
+ "codex_tier": codex_tier,
87
84
  "codex_clean_at": codex_clean_at,
88
85
  }
89
86
  fixture_path.write_text(json.dumps(payload), encoding="utf-8")
@@ -95,7 +92,7 @@ def should_pass_when_codex_required_and_clean(
95
92
  ) -> None:
96
93
  fixture_path = _write_fixture(
97
94
  tmp_path,
98
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
95
+ codex_tier=CODEX_TIER_NORMAL,
99
96
  codex_clean_at=HEAD_SHA,
100
97
  filename="required-clean.json",
101
98
  )
@@ -120,20 +117,18 @@ def should_pass_when_codex_required_and_clean(
120
117
  assert "All pre-conditions met" in captured
121
118
 
122
119
 
123
- def should_pass_without_reading_usage_when_the_clean_stamp_is_on_head(
120
+ def should_pass_without_running_the_picker_when_the_clean_stamp_is_on_head(
124
121
  monkeypatch: pytest.MonkeyPatch,
125
122
  ) -> None:
126
- def _probe_that_must_not_run() -> float | None:
123
+ def _picker_that_must_not_run() -> str | None:
127
124
  raise AssertionError(
128
- "the weekly usage probe must not run when codex_clean_at is on HEAD"
125
+ "the Codex account picker must not run when codex_clean_at is on HEAD"
129
126
  )
130
127
 
131
- monkeypatch.setattr(
132
- check_convergence, "_probe_codex_percent_left", _probe_that_must_not_run
133
- )
128
+ monkeypatch.setattr(check_convergence, "_read_codex_tier", _picker_that_must_not_run)
134
129
 
135
130
  is_passed, detail = check_convergence._evaluate_codex_clean(
136
- read_percent_left=check_convergence._probe_codex_percent_left,
131
+ read_codex_tier=check_convergence._read_codex_tier,
137
132
  codex_clean_at=HEAD_SHA,
138
133
  head_sha=HEAD_SHA,
139
134
  )
@@ -151,7 +146,7 @@ def should_fail_when_codex_clean_stamp_is_shorter_than_an_abbreviated_sha(
151
146
  ) -> None:
152
147
  fixture_path = _write_fixture(
153
148
  tmp_path,
154
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
149
+ codex_tier=CODEX_TIER_NORMAL,
155
150
  codex_clean_at=HEAD_SHA[:1],
156
151
  filename="required-short-stamp.json",
157
152
  )
@@ -179,7 +174,7 @@ def should_pass_when_codex_clean_stamp_is_an_abbreviated_head_sha(
179
174
  ) -> None:
180
175
  fixture_path = _write_fixture(
181
176
  tmp_path,
182
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
177
+ codex_tier=CODEX_TIER_NORMAL,
183
178
  codex_clean_at=HEAD_SHA[:MINIMUM_ABBREVIATED_SHA_LENGTH],
184
179
  filename="required-abbreviated-stamp.json",
185
180
  )
@@ -207,7 +202,7 @@ def should_fail_when_codex_required_and_dirty(
207
202
  ) -> None:
208
203
  fixture_path = _write_fixture(
209
204
  tmp_path,
210
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
205
+ codex_tier=CODEX_TIER_NORMAL,
211
206
  codex_clean_at=OTHER_SHA,
212
207
  filename="required-dirty.json",
213
208
  )
@@ -232,14 +227,15 @@ def should_fail_when_codex_required_and_dirty(
232
227
  assert "do not mark ready" in captured
233
228
 
234
229
 
235
- def should_skip_when_usage_at_or_below_threshold(
236
- tmp_path: Path, capsys: pytest.CaptureFixture[str]
230
+ @pytest.mark.parametrize("tier_without_room", ALL_TIERS_WITHOUT_ROOM_FOR_REVIEW)
231
+ def should_skip_when_the_picker_names_no_account_with_room_for_review(
232
+ tier_without_room: str, tmp_path: Path, capsys: pytest.CaptureFixture[str]
237
233
  ) -> None:
238
234
  fixture_path = _write_fixture(
239
235
  tmp_path,
240
- codex_percent_left=PERCENT_AT_THRESHOLD,
236
+ codex_tier=tier_without_room,
241
237
  codex_clean_at=None,
242
- filename="skipped-threshold.json",
238
+ filename="skipped-tier.json",
243
239
  )
244
240
  exit_code = check_convergence.main(
245
241
  [
@@ -261,12 +257,12 @@ def should_skip_when_usage_at_or_below_threshold(
261
257
  assert "skipped (codex review not required)" in captured
262
258
 
263
259
 
264
- def should_skip_when_codex_percent_left_is_null(
260
+ def should_skip_when_codex_tier_is_null(
265
261
  tmp_path: Path, capsys: pytest.CaptureFixture[str]
266
262
  ) -> None:
267
263
  fixture_path = _write_fixture(
268
264
  tmp_path,
269
- codex_percent_left=None,
265
+ codex_tier=None,
270
266
  codex_clean_at=None,
271
267
  filename="skipped-null.json",
272
268
  )
@@ -297,7 +293,7 @@ def should_bypass_when_codex_token_disables_reviewer(
297
293
  monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "codex")
298
294
  fixture_path = _write_fixture(
299
295
  tmp_path,
300
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
296
+ codex_tier=CODEX_TIER_NORMAL,
301
297
  codex_clean_at=None,
302
298
  filename="skipped-token.json",
303
299
  )
@@ -326,7 +322,7 @@ def should_bypass_when_codex_down_flag_set(
326
322
  ) -> None:
327
323
  fixture_path = _write_fixture(
328
324
  tmp_path,
329
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
325
+ codex_tier=CODEX_TIER_NORMAL,
330
326
  codex_clean_at=None,
331
327
  filename="skipped-down.json",
332
328
  )
@@ -433,7 +429,7 @@ def should_bypass_when_job_state_codex_down_is_true(
433
429
  )
434
430
  fixture_path = _write_fixture(
435
431
  tmp_path,
436
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
432
+ codex_tier=CODEX_TIER_NORMAL,
437
433
  codex_clean_at=None,
438
434
  filename="job-state-codex-down.json",
439
435
  )
@@ -470,7 +466,7 @@ def should_fail_when_job_state_lacks_codex_down_and_clean_stamp(
470
466
  )
471
467
  fixture_path = _write_fixture(
472
468
  tmp_path,
473
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
469
+ codex_tier=CODEX_TIER_NORMAL,
474
470
  codex_clean_at=None,
475
471
  filename="job-state-no-down.json",
476
472
  )
@@ -515,3 +511,49 @@ def should_accept_codex_clean_at_flag_in_parsed_arguments() -> None:
515
511
  ]
516
512
  )
517
513
  assert arguments.codex_clean_at == HEAD_SHA
514
+
515
+
516
+ def _install_fake_picker(
517
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, picker_source: str
518
+ ) -> None:
519
+ picker_path = tmp_path / "codex_account_choice.py"
520
+ picker_path.write_text(picker_source, encoding="utf-8")
521
+ monkeypatch.setattr(check_convergence, "_codex_account_picker_path", lambda: picker_path)
522
+
523
+
524
+ def should_read_the_tier_the_picker_chooses(
525
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
526
+ ) -> None:
527
+ _install_fake_picker(
528
+ tmp_path,
529
+ monkeypatch,
530
+ "import json, sys\n"
531
+ "assert sys.argv[1:] == ['choose']\n"
532
+ "print(json.dumps({'tier': 'normal', 'account': 'codex-2'}))\n",
533
+ )
534
+
535
+ assert check_convergence._read_codex_tier() == CODEX_TIER_NORMAL
536
+
537
+
538
+ @pytest.mark.parametrize(
539
+ "picker_source",
540
+ [
541
+ "import sys\nprint('{\"tier\": \"normal\"}')\nsys.exit(1)\n",
542
+ "print('not json')\n",
543
+ "print('[\"normal\"]')\n",
544
+ "print('{\"tier\": 1}')\n",
545
+ ],
546
+ )
547
+ def should_read_no_tier_when_the_picker_fails_or_answers_out_of_shape(
548
+ picker_source: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
549
+ ) -> None:
550
+ _install_fake_picker(tmp_path, monkeypatch, picker_source)
551
+
552
+ assert check_convergence._read_codex_tier() is None
553
+
554
+
555
+ def should_locate_the_packaged_codex_account_picker() -> None:
556
+ picker_path = check_convergence._codex_account_picker_path()
557
+
558
+ assert picker_path.name == "codex_account_choice.py"
559
+ assert picker_path.is_file()
@@ -118,14 +118,14 @@ def _refuse_bugteam_clean(**_call_keywords: object) -> tuple[bool, str]:
118
118
  )
119
119
 
120
120
 
121
- def _null_codex_percent_left() -> float | None:
121
+ def _unread_codex_tier() -> str | None:
122
122
  return None
123
123
 
124
124
 
125
125
  def _patch_gates_clean(monkeypatch: pytest.MonkeyPatch) -> None:
126
126
  monkeypatch.setattr(check_convergence, "_get_pr_head_sha", _clean_head)
127
127
  monkeypatch.setattr(
128
- check_convergence, "_probe_codex_percent_left", _null_codex_percent_left
128
+ check_convergence, "_read_codex_tier", _unread_codex_tier
129
129
  )
130
130
  for each_gate_name in _ALL_LEAF_GATE_NAMES:
131
131
  monkeypatch.setattr(check_convergence, each_gate_name, _clean_gate)
@@ -127,10 +127,10 @@ def test_path_contains_glob_metacharacters_accepts_windows_paths_with_parens() -
127
127
  `(`, `)`, and `,` are not glob metacharacters in Claude Code's permission
128
128
  rule matching. Including them in the metacharacter set causes
129
129
  get_current_project_path to raise ValueError for any user whose home
130
- directory contains parentheses (e.g. `C:/Users/Jon (Admin)/...`).
130
+ directory contains parentheses (e.g. `C:/Users/Example (Admin)/...`).
131
131
  """
132
132
  assert common.path_contains_glob_metacharacters("C:/Program Files (x86)/app") is False
133
- assert common.path_contains_glob_metacharacters("C:/Users/Jon (Admin)/project") is False
133
+ assert common.path_contains_glob_metacharacters("C:/Users/Example (Admin)/project") is False
134
134
  assert common.path_contains_glob_metacharacters("C:/Projects/a,b/file.py") is False
135
135
 
136
136
 
@@ -227,7 +227,7 @@ test('generated advisory shims keep temporary Git commit and push available afte
227
227
  const { temporaryRoot, gitHooksDirectory } = makeTemporaryGitHooksDirectory();
228
228
  const repositoryRoot = join(temporaryRoot, 'repository');
229
229
  const bareRepositoryRoot = join(temporaryRoot, 'remote.git');
230
- const targetRemote = 'https://github.com/JonEcho/python-automation.git';
230
+ const targetRemote = 'https://github.com/example-owner/example-repo.git';
231
231
  try {
232
232
  writeFileSync(
233
233
  join(gitHooksDirectory, 'broken_advisory.py'),
@@ -159,15 +159,10 @@ export const CODEX_RULES_SHIPPED_FILE_NAME = 'claude-dev-env.rules';
159
159
  export const DEFAULT_CURSOR_DIRECTORY_NAME = '.cursor';
160
160
 
161
161
  /**
162
- * Directory name under the Cursor home that holds generated `.mdc` rule files.
162
+ * Directory name for rule files, under the Cursor home and under the agents home.
163
163
  */
164
164
  export const CURSOR_RULES_DIRECTORY_NAME = 'rules';
165
165
 
166
- /**
167
- * Installed script that writes Cursor `.mdc` files from Claude rules.
168
- */
169
- export const CURSOR_SYNC_SCRIPT_FILE_NAME = 'sync_to_cursor.py';
170
-
171
166
  /**
172
167
  * Windows Python launcher command the installer may bake into hook settings.
173
168
  */