claude-dev-env 8.12.0 → 8.14.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 (85) hide show
  1. package/.agents/skills/pull-request/scripts/test_pull_request.py +1 -1
  2. package/.agents/skills/team-advisor/SKILL.md +1 -1
  3. package/.agents/skills-archived/pr-converge/scripts/test_check_convergence.py +4 -4
  4. package/_shared/advisor/advisor-protocol.md +19 -19
  5. package/_shared/advisor/reference/advisor-block.md +1 -1
  6. package/_shared/advisor/reference/astra-rung.md +11 -11
  7. package/_shared/advisor/reference/identity.md +4 -4
  8. package/_shared/advisor/reference/lifecycle.md +2 -2
  9. package/_shared/advisor/reference/spawn-walk-log.md +4 -4
  10. package/_shared/advisor/reference/third-party-bind.md +7 -7
  11. package/_shared/advisor/reference/warm-up.md +3 -3
  12. package/_shared/advisor/scripts/codex_astra_advisor.py +33 -26
  13. package/_shared/advisor/scripts/codex_astra_preflight.py +57 -55
  14. package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +2 -2
  15. package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +8 -8
  16. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +3 -3
  17. package/_shared/advisor/scripts/model_tier_run_validator.py +8 -9
  18. package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +69 -23
  19. package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +4 -4
  20. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +91 -91
  21. package/_shared/pr-loop/scripts/check_convergence.py +54 -49
  22. package/_shared/pr-loop/scripts/codex_review_scripts_constants/run_constants.py +4 -0
  23. package/_shared/pr-loop/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +10 -1
  24. package/_shared/pr-loop/scripts/run_codex_review.py +4 -6
  25. package/_shared/pr-loop/scripts/test_check_convergence.py +6 -6
  26. package/_shared/pr-loop/scripts/test_check_convergence_codex.py +73 -31
  27. package/_shared/pr-loop/scripts/test_check_convergence_contract.py +2 -2
  28. package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +2 -2
  29. package/bin/git_hooks_installer.test.mjs +1 -1
  30. package/bin/install-constants.mjs +1 -6
  31. package/bin/install.cursor-rules.test.mjs +24 -51
  32. package/bin/install.mjs +16 -70
  33. package/docs/codex-accounts.md +79 -0
  34. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +5 -5
  35. package/hooks/blocking/test_pii_scanner.py +1 -1
  36. package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +2 -2
  37. package/hooks/git-hooks/conftest.py +16 -0
  38. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +1 -1
  39. package/hooks/git-hooks/test_post_commit.py +1 -1
  40. package/hooks/git-hooks/test_pre_commit.py +1 -1
  41. package/hooks/git-hooks/test_pre_push.py +2 -2
  42. package/hooks/git-hooks/test_verification_start.py +1 -1
  43. package/hooks/git-hooks/verification_notice.py +2 -2
  44. package/hooks/git-hooks/verification_notice_context.py +21 -2
  45. package/hooks/git-hooks/verification_start.py +2 -3
  46. package/package.json +1 -1
  47. package/rules/durable-post-artifacts.md +4 -3
  48. package/scripts/claude_account_profile.py +21 -9
  49. package/scripts/codex_account_choice.py +396 -0
  50. package/scripts/codex_account_meters.py +372 -0
  51. package/scripts/dev_env_scripts_constants/codex_account_constants.py +141 -0
  52. package/scripts/dev_env_scripts_constants/private_term_constants.py +17 -2
  53. package/scripts/private_term_scan.py +6 -2
  54. package/scripts/repository_checks/config/constants.py +1 -0
  55. package/scripts/repository_checks/tracked_private_terms.py +7 -0
  56. package/scripts/test_claude_account_profile.py +12 -0
  57. package/scripts/test_codex_account_choice.py +276 -0
  58. package/scripts/test_codex_account_meters.py +205 -0
  59. package/scripts/tests/test_automatic_advisory.py +3 -3
  60. package/scripts/tests/test_automatic_advisory_polling.py +1 -1
  61. package/scripts/tests/test_automatic_advisory_recovery.py +1 -1
  62. package/scripts/tests/test_closed_pr_label.py +1 -1
  63. package/scripts/tests/test_private_term_scan.py +28 -0
  64. package/scripts/tests/test_review_closure.py +2 -2
  65. package/scripts/tests/test_review_closure_model.py +2 -2
  66. package/scripts/tests/test_rules.py +2 -83
  67. package/scripts/tests/test_tracked_private_terms.py +15 -0
  68. package/scripts/tests/test_verification_notice.py +29 -13
  69. package/.agents/skills/cert-classification-rule/SKILL.md +0 -224
  70. package/.agents/skills/cert-classification-rule/scripts/resort_cert_folders.py +0 -158
  71. package/_shared/pr-loop/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -82
  72. package/_shared/pr-loop/scripts/codex_usage_probe.py +0 -573
  73. package/_shared/pr-loop/scripts/test_codex_usage_probe.py +0 -683
  74. package/scripts/sync_to_cursor/AGENTS.md +0 -1
  75. package/scripts/sync_to_cursor/CLAUDE.md +0 -1
  76. package/scripts/sync_to_cursor/__init__.py +0 -13
  77. package/scripts/sync_to_cursor/canonical_docs.py +0 -66
  78. package/scripts/sync_to_cursor/config/__init__.py +0 -8
  79. package/scripts/sync_to_cursor/engine.py +0 -219
  80. package/scripts/sync_to_cursor/hashing.py +0 -7
  81. package/scripts/sync_to_cursor/paths.py +0 -18
  82. package/scripts/sync_to_cursor/rules.py +0 -494
  83. package/scripts/sync_to_cursor.py +0 -22
  84. package/scripts/tests/test_engine.py +0 -101
  85. package/scripts/tests/test_sync_to_cursor.py +0 -315
@@ -1,8 +1,7 @@
1
- """Evaluate Codex usage before an Astra advisor request."""
1
+ """Ask the Codex account picker for an account before an Astra advisor request."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import importlib
6
5
  import json
7
6
  import math
8
7
  import subprocess
@@ -12,11 +11,13 @@ from dataclasses import dataclass
12
11
  from pathlib import Path
13
12
 
14
13
  from advisor_scripts_constants.astra_advisor_constants import (
14
+ ACCOUNT_PICKER_CHOOSE_COMMAND,
15
+ ASTRA_ACCOUNT_PICK_TIMEOUT_REASON,
16
+ ASTRA_ACCOUNT_PICK_TIMEOUT_SECONDS,
15
17
  ASTRA_FALLBACK_KIND_BROKEN,
16
18
  ASTRA_FALLBACK_KIND_DECLINED,
17
19
  ASTRA_PREFLIGHT_FAILURE_REASON,
18
- ASTRA_PROBE_TIMEOUT_REASON,
19
- ASTRA_USAGE_PROBE_TIMEOUT_SECONDS,
20
+ CODEX_TIER_NORMAL,
20
21
  )
21
22
 
22
23
 
@@ -26,6 +27,7 @@ class AstraPreflight:
26
27
  percent_left: float | None
27
28
  reason: str
28
29
  fallback_kind: str | None = None
30
+ codex_home: Path | None = None
29
31
 
30
32
 
31
33
  def _preflight_fallback(
@@ -34,82 +36,82 @@ def _preflight_fallback(
34
36
  return AstraPreflight(False, percent_left, reason, fallback_kind)
35
37
 
36
38
 
37
- def _load_usage_gate(probe_path: Path) -> Callable[[float], bool]:
38
- probe_directory = str(probe_path.parent)
39
- if probe_directory not in sys.path:
40
- sys.path.insert(0, probe_directory)
41
- return importlib.import_module("codex_usage_probe").is_codex_review_required
39
+ def _broken(detail: str) -> AstraPreflight:
40
+ return _preflight_fallback(
41
+ f"{ASTRA_PREFLIGHT_FAILURE_REASON}: {detail}", None, ASTRA_FALLBACK_KIND_BROKEN
42
+ )
42
43
 
43
44
 
44
- def _parse_probe_percent(stdout_text: str) -> tuple[float | None, str | None]:
45
- try:
46
- report = json.loads(stdout_text)
47
- except (TypeError, json.JSONDecodeError):
48
- return None, "usage report is malformed"
49
- if not isinstance(report, dict):
50
- return None, "usage report is malformed"
51
- raw_percent = report.get("percent_left")
45
+ def _finite_percent(raw_percent: object) -> float | None:
52
46
  if isinstance(raw_percent, bool) or not isinstance(raw_percent, (int, float)):
53
- return None, "usage meter is unknown" if raw_percent is None else "usage meter is malformed"
54
- percent_left = float(raw_percent)
55
- if not math.isfinite(percent_left):
56
- return None, "usage meter is malformed"
57
- return percent_left, None
58
-
59
-
60
- def _run_probe(
61
- probe_path: Path,
47
+ return None
48
+ return float(raw_percent) if math.isfinite(raw_percent) else None
49
+
50
+
51
+ def _preflight_from_answer(field_by_name: dict[str, object]) -> AstraPreflight:
52
+ if not isinstance(field_by_name.get("tier"), str):
53
+ return _broken("picker answer is malformed")
54
+ if field_by_name["tier"] != CODEX_TIER_NORMAL:
55
+ reason = f"{ASTRA_PREFLIGHT_FAILURE_REASON}: no Codex account has room ({field_by_name.get('reason')})"
56
+ return _preflight_fallback(reason, None, ASTRA_FALLBACK_KIND_DECLINED)
57
+ codex_home = field_by_name.get("codex_home")
58
+ percent_left = _finite_percent(field_by_name.get("percent_left"))
59
+ if not isinstance(codex_home, str) or not codex_home or percent_left is None:
60
+ return _broken("picker answer names no Codex home with room")
61
+ reason = f"{field_by_name.get('account')} has {percent_left:.0f}% left"
62
+ return AstraPreflight(True, percent_left, reason, codex_home=Path(codex_home))
63
+
64
+
65
+ def _run_picker(
66
+ picker_path: Path,
62
67
  process_runner: Callable[..., subprocess.CompletedProcess[str]],
63
68
  ) -> subprocess.CompletedProcess[str]:
64
69
  return process_runner(
65
- [sys.executable, str(probe_path)],
70
+ [sys.executable, str(picker_path), ACCOUNT_PICKER_CHOOSE_COMMAND],
66
71
  capture_output=True,
67
72
  text=True,
68
73
  check=False,
69
74
  shell=False,
70
- timeout=ASTRA_USAGE_PROBE_TIMEOUT_SECONDS,
75
+ timeout=ASTRA_ACCOUNT_PICK_TIMEOUT_SECONDS,
71
76
  )
72
77
 
73
78
 
74
- def _preflight_from_probe(
75
- probe_path: Path, completed: subprocess.CompletedProcess[str]
76
- ) -> AstraPreflight:
79
+ def _preflight_from_picker(completed: subprocess.CompletedProcess[str]) -> AstraPreflight:
77
80
  if completed.returncode != 0:
78
- reason = f"{ASTRA_PREFLIGHT_FAILURE_REASON}: probe exit {completed.returncode}"
79
- return _preflight_fallback(reason, None, ASTRA_FALLBACK_KIND_BROKEN)
80
- percent_left, parse_reason = _parse_probe_percent(completed.stdout)
81
- if parse_reason is not None or percent_left is None:
82
- reason = f"{ASTRA_PREFLIGHT_FAILURE_REASON}: {parse_reason or 'usage meter is unknown'}"
83
- return _preflight_fallback(reason, None, ASTRA_FALLBACK_KIND_BROKEN)
84
- if not _load_usage_gate(probe_path)(percent_left):
85
- reason = f"{ASTRA_PREFLIGHT_FAILURE_REASON}: usage meter is at or below the gate"
86
- return _preflight_fallback(reason, percent_left, ASTRA_FALLBACK_KIND_DECLINED)
87
- return AstraPreflight(True, percent_left, "usage meter is above the Astra gate")
81
+ return _broken(f"picker exit {completed.returncode}")
82
+ try:
83
+ answer = json.loads(completed.stdout)
84
+ except (TypeError, json.JSONDecodeError):
85
+ return _broken("picker answer is malformed")
86
+ if not isinstance(answer, dict):
87
+ return _broken("picker answer is malformed")
88
+ return _preflight_from_answer(answer)
88
89
 
89
90
 
90
91
  def run_astra_preflight(
91
- probe_path: Path,
92
+ picker_path: Path,
92
93
  process_runner: Callable[..., subprocess.CompletedProcess[str]],
93
94
  ) -> AstraPreflight:
94
- """Run the usage probe and decide whether Astra may bind.
95
+ """Run the Codex account picker and decide whether Astra may bind.
96
+
97
+ ::
98
+
99
+ tier normal, codex-2, 62% left -> eligible, codex_home of codex-2
100
+ tier luna or wait -> declined
101
+ picker exit 1, bad JSON, timeout -> broken
95
102
 
96
103
  Args:
97
- probe_path: Path to the installed usage probe.
98
- process_runner: Callable that runs the probe.
104
+ picker_path: Path to ``codex_account_choice.py``.
105
+ process_runner: Callable that runs the picker.
99
106
 
100
107
  Returns:
101
- Eligibility, remaining usage percent, reason, and fallback kind.
108
+ Eligibility, the chosen account's percent left and Codex home, reason, and fallback kind.
102
109
  """
103
110
  try:
104
- completed = _run_probe(probe_path, process_runner)
105
- return _preflight_from_probe(probe_path, completed)
111
+ return _preflight_from_picker(_run_picker(picker_path, process_runner))
106
112
  except subprocess.TimeoutExpired as error:
107
113
  return _preflight_fallback(
108
- f"{ASTRA_PROBE_TIMEOUT_REASON}: {error}", None, ASTRA_FALLBACK_KIND_BROKEN
109
- )
110
- except (OSError, subprocess.SubprocessError, ImportError, AttributeError, TypeError, ValueError) as error:
111
- return _preflight_fallback(
112
- f"{ASTRA_PREFLIGHT_FAILURE_REASON}: {error}",
113
- None,
114
- ASTRA_FALLBACK_KIND_BROKEN,
114
+ f"{ASTRA_ACCOUNT_PICK_TIMEOUT_REASON}: {error}", None, ASTRA_FALLBACK_KIND_BROKEN
115
115
  )
116
+ except (OSError, subprocess.SubprocessError, TypeError, ValueError) as error:
117
+ return _broken(str(error))
@@ -6,8 +6,8 @@ import json
6
6
  from pathlib import Path
7
7
 
8
8
  ADVISOR_MODEL_TIER: str = "Astra"
9
- ADVISOR_FALLBACK_TIER: str = "Fable"
10
- ADVISOR_FALLBACK_RESULT: str = "fable"
9
+ ADVISOR_FALLBACK_TIER: str = "Opus"
10
+ ADVISOR_FALLBACK_RESULT: str = "opus"
11
11
  ADVISOR_EFFORT_ENV_VAR: str = "ADVISOR_EFFORT"
12
12
  ADVISOR_ROUTING_TIMEOUT_SECONDS: int = 10
13
13
 
@@ -13,20 +13,20 @@ CODEX_PROMPT_FROM_STDIN: str = "-"
13
13
  CODEX_EXEC_SUBCOMMAND: str = "exec"
14
14
  CODEX_RESUME_SUBCOMMAND: str = "resume"
15
15
  CODEX_SANDBOX_FLAG: str = "--sandbox"
16
- CLAUDE_CONFIG_DIRECTORY_NAME: str = ".claude"
17
- USAGE_PROBE_SHARED_DIRECTORY_NAME: str = "_shared"
18
- USAGE_PROBE_PACKAGE_DIRECTORY_NAME: str = "pr-loop"
19
- USAGE_PROBE_SCRIPTS_DIRECTORY_NAME: str = "scripts"
20
- USAGE_PROBE_FILENAME: str = "codex_usage_probe.py"
16
+ CODEX_HOME_ENV_VAR: str = "CODEX_HOME"
17
+ ALL_ACCOUNT_PICKER_RELATIVE_PARTS: tuple[str, ...] = ("scripts", "codex_account_choice.py")
18
+ SHARED_PACKAGE_ROOT_PARENT_INDEX: int = 2
19
+ ACCOUNT_PICKER_CHOOSE_COMMAND: str = "choose"
20
+ CODEX_TIER_NORMAL: str = "normal"
21
21
  ASTRA_SESSION_ID_METAVAR: str = "SESSION_ID"
22
22
  ASTRA_CODEX_TIMEOUT_SECONDS: float = 120.0
23
- ASTRA_USAGE_PROBE_TIMEOUT_SECONDS: float = 30.0
23
+ ASTRA_ACCOUNT_PICK_TIMEOUT_SECONDS: float = 150.0
24
24
  ASTRA_ENV_VAR: str = "ADVISOR_ASTRA"
25
25
  ALL_ASTRA_TRUTHY_VALUES: frozenset[str] = frozenset({"1", "true", "yes", "on"})
26
- ASTRA_PREFLIGHT_FAILURE_REASON: str = "astra preflight did not establish an eligible Codex meter"
26
+ ASTRA_PREFLIGHT_FAILURE_REASON: str = "astra preflight found no Codex account to bind"
27
27
  ASTRA_BIND_FAILURE_REASON: str = "Codex Astra bind failed"
28
28
  ASTRA_REPLY_FAILURE_REASON: str = "Codex Astra returned no advisor guidance"
29
- ASTRA_PROBE_TIMEOUT_REASON: str = "Codex Astra meter check timed out"
29
+ ASTRA_ACCOUNT_PICK_TIMEOUT_REASON: str = "Codex Astra account pick timed out"
30
30
  ASTRA_CODEX_TIMEOUT_REASON: str = "Codex Astra request timed out"
31
31
  ASTRA_MALFORMED_JSONL_REASON: str = "Codex Astra returned malformed JSONL"
32
32
  ASTRA_MISSING_SESSION_REASON: str = "Codex Astra returned no session id"
@@ -43,8 +43,8 @@ SESSION_IDENTITY_WORD_PATTERN: str = r"[a-z0-9]+"
43
43
  HOST_PROFILE_JSON_KEY: str = "host_profile"
44
44
 
45
45
  ALL_MODEL_TIERS: tuple[str, ...] = (
46
+ "Fable",
46
47
  ADVISOR_FALLBACK_TIER,
47
- "Opus",
48
48
  "Sonnet",
49
49
  "Haiku",
50
50
  )
@@ -58,8 +58,8 @@ ALL_KNOWN_TIER_NAMES: tuple[str, ...] = (
58
58
  ADVISOR_SENDMESSAGE_REPLY_WAIT_SECONDS: int = 120
59
59
 
60
60
  ALL_CLI_MODEL_ID_BY_TIER: dict[str, str] = {
61
- ADVISOR_FALLBACK_TIER: "fable",
62
- "Opus": "opus",
61
+ "Fable": "fable",
62
+ ADVISOR_FALLBACK_TIER: "opus",
63
63
  "Sonnet": "sonnet",
64
64
  "Haiku": "haiku",
65
65
  THIRD_PARTY_MODEL_TIER: "third-party",
@@ -7,17 +7,17 @@ This validator reads that log back and checks its invariants.
7
7
 
8
8
  ladder_walk = ModelTierRun(
9
9
  own_tier="Opus",
10
- candidate_tiers=["Fable"],
11
- attempts=[{"tier": "Fable", "result": "spawned"}],
12
- selected_tier="Fable",
10
+ candidate_tiers=["Opus"],
11
+ attempts=[{"tier": "Opus", "result": "spawned"}],
12
+ selected_tier="Opus",
13
13
  )
14
14
  validate_model_tier_run(ladder_walk) # ok: returns None, raises nothing
15
15
 
16
16
  cli_bind = ModelTierRun(
17
17
  own_tier="Opus",
18
- candidate_tiers=["Fable"],
19
- attempts=[{"tier": "Fable", "result": "cli"}],
20
- selected_tier="Fable",
18
+ candidate_tiers=["Opus"],
19
+ attempts=[{"tier": "Opus", "result": "cli"}],
20
+ selected_tier="Opus",
21
21
  )
22
22
  validate_model_tier_run(cli_bind) # ok: third-party-host CLI Claude-chain bind
23
23
 
@@ -330,10 +330,9 @@ def validate_model_tier_run(run: ModelTierRun) -> None:
330
330
  validate_model_tier_run(cli_bind) # ok: CLI Claude-chain bind
331
331
  validate_model_tier_run(broken_log) # flag: ModelTierRunError
332
332
 
333
- Candidate tiers are Fable, plus Astra when ``is_astra_enabled`` is true, on
333
+ Candidate tiers are Opus, plus Astra when ``is_astra_enabled`` is true, on
334
334
  Claude and ThirdParty hosts. A Codex host walks Astra only. Consumer
335
- ``own_tier`` is recorded and must be a known tier. It does not add Opus
336
- to the advisor walk. Tries walk that list in order. Early stop only
335
+ ``own_tier`` is recorded and must be a known tier. Tries walk that list in order. Early stop only
337
336
  after ``spawned``, ``cli``, or Astra ``codex`` (and Astra ``spawned`` on a
338
337
  Codex host). A null selected_tier requires a full walk plus
339
338
  fallback_reason.
@@ -31,9 +31,15 @@ def _load_astra_module() -> ModuleType:
31
31
  astra_advisor = _load_astra_module()
32
32
 
33
33
  SCRIPTS_ROOT = Path(__file__).parent.parent
34
- USAGE_PROBE_PATH = (
35
- SCRIPTS_ROOT.parents[1] / "pr-loop" / "scripts" / "codex_usage_probe.py"
36
- )
34
+ PICKER_PATH = SCRIPTS_ROOT.parents[2] / "scripts" / "codex_account_choice.py"
35
+ PICKED_CODEX_HOME = Path("codex-profiles") / "codex-2"
36
+ NORMAL_ANSWER = {
37
+ "tier": "normal",
38
+ "account": "codex-2",
39
+ "codex_home": str(PICKED_CODEX_HOME),
40
+ "percent_left": 90,
41
+ "reason": "codex-2 has 90% left",
42
+ }
37
43
  ENABLED_SETTINGS = {astra_advisor.ASTRA_ENV_VAR: "1"}
38
44
 
39
45
 
@@ -42,9 +48,9 @@ def _codex_on_search_path(monkeypatch: pytest.MonkeyPatch) -> None:
42
48
  monkeypatch.setattr(astra_advisor.shutil, "which", lambda name: "codex")
43
49
 
44
50
 
45
- def _probe(payload: object, returncode: int = 0) -> subprocess.CompletedProcess[str]:
51
+ def _pick(payload: object, returncode: int = 0) -> subprocess.CompletedProcess[str]:
46
52
  return subprocess.CompletedProcess(
47
- [sys.executable, str(USAGE_PROBE_PATH)],
53
+ [sys.executable, str(PICKER_PATH), "choose"],
48
54
  returncode,
49
55
  json.dumps(payload),
50
56
  "",
@@ -66,14 +72,18 @@ def _events(session_id: str = "thread-1", guidance: str = "PLAN\ninspect") -> st
66
72
 
67
73
 
68
74
  def _two_step_runner(
69
- all_calls: list[list[str]], guidance: str = "PLAN\ninspect"
75
+ all_calls: list[list[str]],
76
+ guidance: str = "PLAN\ninspect",
77
+ all_call_keywords: list[dict[str, object]] | None = None,
70
78
  ) -> _ProcessRunner:
71
79
  def runner(
72
80
  arguments: list[str], **kwargs: object
73
81
  ) -> subprocess.CompletedProcess[str]:
74
82
  all_calls.append(arguments)
83
+ if all_call_keywords is not None:
84
+ all_call_keywords.append(kwargs)
75
85
  if len(all_calls) == 1:
76
- return _probe({"percent_left": 90})
86
+ return _pick(NORMAL_ANSWER)
77
87
  return subprocess.CompletedProcess(arguments, 0, _events(guidance=guidance), "")
78
88
  return runner
79
89
 
@@ -155,31 +165,63 @@ def test_argument_parser_accepts_astra_and_rejects_sol() -> None:
155
165
  parser.parse_args(["--bind", "--cwd", ".", "--enable-sol"])
156
166
 
157
167
 
158
- def test_usage_probe_path_uses_supplied_home(tmp_path: Path) -> None:
159
- path = astra_advisor.resolve_usage_probe_path(tmp_path)
160
- assert path == tmp_path / ".claude" / "_shared" / "pr-loop" / "scripts" / "codex_usage_probe.py"
168
+ def test_account_picker_path_names_the_packaged_picker() -> None:
169
+ path = astra_advisor.resolve_account_picker_path()
170
+ assert path == PICKER_PATH.resolve()
171
+ assert path.is_file()
172
+
173
+
174
+ def test_preflight_runs_the_picker_choose_command() -> None:
175
+ all_calls: list[list[str]] = []
176
+
177
+ def runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
178
+ all_calls.append(arguments)
179
+ return _pick(NORMAL_ANSWER)
161
180
 
181
+ astra_advisor.run_astra_preflight(PICKER_PATH, runner)
182
+ assert all_calls == [[sys.executable, str(PICKER_PATH), "choose"]]
162
183
 
163
- def test_preflight_accepts_meter_above_gate() -> None:
184
+
185
+ def test_preflight_accepts_the_normal_tier_and_keeps_its_codex_home() -> None:
164
186
  preflight = astra_advisor.run_astra_preflight(
165
- USAGE_PROBE_PATH, lambda arguments, **kwargs: _probe({"percent_left": 90})
187
+ PICKER_PATH, lambda arguments, **kwargs: _pick(NORMAL_ANSWER)
166
188
  )
167
189
  assert preflight.eligible
168
190
  assert preflight.percent_left == 90
191
+ assert preflight.codex_home == PICKED_CODEX_HOME
169
192
 
170
193
 
171
- def test_preflight_declines_meter_at_gate() -> None:
194
+ @pytest.mark.parametrize("tier", ["luna", "wait"])
195
+ def test_preflight_declines_a_tier_without_room(tier: str) -> None:
196
+ answer = {**NORMAL_ANSWER, "tier": tier, "reason": "every account is low"}
172
197
  preflight = astra_advisor.run_astra_preflight(
173
- USAGE_PROBE_PATH, lambda arguments, **kwargs: _probe({"percent_left": 10})
198
+ PICKER_PATH, lambda arguments, **kwargs: _pick(answer)
174
199
  )
175
200
  assert not preflight.eligible
176
201
  assert preflight.fallback_kind == astra_advisor.ASTRA_FALLBACK_KIND_DECLINED
202
+ assert preflight.codex_home is None
177
203
 
178
204
 
179
- @pytest.mark.parametrize("payload", [{"percent_left": None}, {"percent_left": "90"}, ["bad"]])
180
- def test_preflight_rejects_malformed_meter(payload: object) -> None:
205
+ @pytest.mark.parametrize(
206
+ "payload",
207
+ [
208
+ {"tier": None},
209
+ {**NORMAL_ANSWER, "codex_home": None},
210
+ {**NORMAL_ANSWER, "percent_left": "90"},
211
+ ["bad"],
212
+ ],
213
+ )
214
+ def test_preflight_rejects_a_malformed_answer(payload: object) -> None:
215
+ preflight = astra_advisor.run_astra_preflight(
216
+ PICKER_PATH, lambda arguments, **kwargs: _pick(payload)
217
+ )
218
+ assert not preflight.eligible
219
+ assert preflight.fallback_kind == astra_advisor.ASTRA_FALLBACK_KIND_BROKEN
220
+
221
+
222
+ def test_preflight_rejects_a_failed_picker() -> None:
181
223
  preflight = astra_advisor.run_astra_preflight(
182
- USAGE_PROBE_PATH, lambda arguments, **kwargs: _probe(payload)
224
+ PICKER_PATH, lambda arguments, **kwargs: _pick(NORMAL_ANSWER, returncode=1)
183
225
  )
184
226
  assert not preflight.eligible
185
227
  assert preflight.fallback_kind == astra_advisor.ASTRA_FALLBACK_KIND_BROKEN
@@ -208,17 +250,21 @@ def test_jsonl_parser_falls_back_for_invalid_reply(jsonl_text: str) -> None:
208
250
  assert not reply.successful
209
251
 
210
252
 
211
- def test_bind_runs_probe_then_codex() -> None:
253
+ def test_bind_runs_picker_then_codex_under_the_chosen_home() -> None:
212
254
  calls: list[list[str]] = []
255
+ all_call_keywords: list[dict[str, object]] = []
213
256
  reply = astra_advisor.run_codex_astra_advisor(
214
257
  "consult",
215
258
  Path("."),
216
259
  None,
217
- USAGE_PROBE_PATH,
260
+ PICKER_PATH,
218
261
  ENABLED_SETTINGS,
219
262
  None,
220
- _two_step_runner(calls),
263
+ _two_step_runner(calls, all_call_keywords=all_call_keywords),
221
264
  )
265
+ codex_environment = all_call_keywords[1]["env"]
266
+ assert isinstance(codex_environment, dict)
267
+ assert codex_environment["CODEX_HOME"] == str(PICKED_CODEX_HOME)
222
268
  assert reply.successful
223
269
  assert reply.selected_tier == "Astra"
224
270
  assert reply.outcome == "codex"
@@ -236,7 +282,7 @@ def test_bind_routes_astra_xhigh_to_the_policy_effort() -> None:
236
282
  "consult",
237
283
  Path("."),
238
284
  None,
239
- USAGE_PROBE_PATH,
285
+ PICKER_PATH,
240
286
  settings,
241
287
  None,
242
288
  _two_step_runner(calls),
@@ -263,7 +309,7 @@ def test_disabled_flag_returns_declined_fallback() -> None:
263
309
  def test_missing_codex_returns_broken_fallback(monkeypatch: pytest.MonkeyPatch) -> None:
264
310
  monkeypatch.setattr(astra_advisor.shutil, "which", lambda name: None)
265
311
  reply = astra_advisor.run_codex_astra_advisor(
266
- "consult", Path("."), None, USAGE_PROBE_PATH, ENABLED_SETTINGS, None, subprocess.run
312
+ "consult", Path("."), None, PICKER_PATH, ENABLED_SETTINGS, None, subprocess.run
267
313
  )
268
314
  assert reply.is_fallback
269
315
  assert reply.fallback_kind == astra_advisor.ASTRA_FALLBACK_KIND_BROKEN
@@ -275,7 +321,7 @@ def test_resume_uses_existing_session() -> None:
275
321
  "resume",
276
322
  Path("."),
277
323
  None,
278
- USAGE_PROBE_PATH,
324
+ PICKER_PATH,
279
325
  ENABLED_SETTINGS,
280
326
  "thread-1",
281
327
  _two_step_runner(calls, "ENDORSE\nready"),
@@ -1,4 +1,4 @@
1
- """Tests for Codex Astra usage preflight."""
1
+ """Tests for the Codex Astra account preflight."""
2
2
 
3
3
  import json
4
4
  import subprocess
@@ -12,13 +12,13 @@ sys.path.insert(0, str(SCRIPTS_ROOT))
12
12
  from codex_astra_preflight import run_astra_preflight
13
13
 
14
14
 
15
- def test_run_astra_preflight_rejects_probe_failure(tmp_path: Path) -> None:
16
- probe_path = tmp_path / "codex_usage_probe.py"
15
+ def test_run_astra_preflight_rejects_picker_failure(tmp_path: Path) -> None:
16
+ picker_path = tmp_path / "codex_account_choice.py"
17
17
 
18
18
  def process_runner(*args: object, **kwargs: object) -> subprocess.CompletedProcess[str]:
19
19
  return subprocess.CompletedProcess([], 1, json.dumps({}), "")
20
20
 
21
- preflight = run_astra_preflight(probe_path, process_runner)
21
+ preflight = run_astra_preflight(picker_path, process_runner)
22
22
 
23
23
  assert not preflight.eligible
24
24
  assert preflight.fallback_kind == "broken"