claude-dev-env 8.11.1 → 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 (105) hide show
  1. package/.agents/skills/e-code-review/SKILL.md +24 -71
  2. package/.agents/skills/e-code-review/reference/fix.md +7 -78
  3. package/.agents/skills/e-code-review/reference/high.md +129 -0
  4. package/.agents/skills/e-code-review/reference/low.md +20 -51
  5. package/.agents/skills/e-code-review/reference/max.md +169 -0
  6. package/.agents/skills/e-code-review/reference/medium.md +7 -191
  7. package/.agents/skills/e-code-review/reference/xhigh.md +47 -91
  8. package/.agents/skills/pull-request/scripts/test_pull_request.py +1 -1
  9. package/.agents/skills-archived/pr-converge/scripts/test_check_convergence.py +4 -4
  10. package/_shared/advisor/reference/astra-rung.md +7 -7
  11. package/_shared/advisor/reference/third-party-bind.md +2 -2
  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/astra_advisor_constants.py +8 -8
  15. package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +69 -23
  16. package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +4 -4
  17. package/_shared/pr-loop/scripts/check_convergence.py +54 -49
  18. package/_shared/pr-loop/scripts/codex_review_scripts_constants/run_constants.py +4 -0
  19. package/_shared/pr-loop/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +10 -1
  20. package/_shared/pr-loop/scripts/run_codex_review.py +4 -6
  21. package/_shared/pr-loop/scripts/test_check_convergence.py +6 -6
  22. package/_shared/pr-loop/scripts/test_check_convergence_codex.py +73 -31
  23. package/_shared/pr-loop/scripts/test_check_convergence_contract.py +2 -2
  24. package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +2 -2
  25. package/bin/git_hooks_installer.test.mjs +1 -1
  26. package/bin/install-constants.mjs +1 -6
  27. package/bin/install.agents-home.test.mjs +9 -9
  28. package/bin/install.cursor-rules.test.mjs +24 -51
  29. package/bin/install.mjs +16 -70
  30. package/commands/sr-loop.md +3 -1
  31. package/docs/codex-accounts.md +79 -0
  32. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +5 -5
  33. package/hooks/blocking/test_pii_scanner.py +1 -1
  34. package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +2 -2
  35. package/hooks/git-hooks/conftest.py +16 -0
  36. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +1 -1
  37. package/hooks/git-hooks/test_post_commit.py +1 -1
  38. package/hooks/git-hooks/test_pre_commit.py +1 -1
  39. package/hooks/git-hooks/test_pre_push.py +2 -2
  40. package/hooks/git-hooks/test_verification_start.py +1 -1
  41. package/hooks/git-hooks/verification_notice.py +2 -2
  42. package/hooks/git-hooks/verification_notice_context.py +21 -2
  43. package/hooks/git-hooks/verification_start.py +2 -3
  44. package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -1
  45. package/package.json +1 -1
  46. package/rules/durable-post-artifacts.md +4 -3
  47. package/scripts/claude_account_profile.py +21 -9
  48. package/scripts/codex_account_choice.py +396 -0
  49. package/scripts/codex_account_meters.py +372 -0
  50. package/scripts/dev_env_scripts_constants/codex_account_constants.py +141 -0
  51. package/scripts/dev_env_scripts_constants/private_term_constants.py +17 -2
  52. package/scripts/private_term_scan.py +6 -2
  53. package/scripts/repository_checks/config/constants.py +1 -0
  54. package/scripts/repository_checks/tracked_private_terms.py +7 -0
  55. package/scripts/test_claude_account_profile.py +12 -0
  56. package/scripts/test_codex_account_choice.py +276 -0
  57. package/scripts/test_codex_account_meters.py +205 -0
  58. package/scripts/tests/test_automatic_advisory.py +3 -3
  59. package/scripts/tests/test_automatic_advisory_polling.py +1 -1
  60. package/scripts/tests/test_automatic_advisory_recovery.py +1 -1
  61. package/scripts/tests/test_closed_pr_label.py +1 -1
  62. package/scripts/tests/test_private_term_scan.py +28 -0
  63. package/scripts/tests/test_review_closure.py +2 -2
  64. package/scripts/tests/test_review_closure_model.py +2 -2
  65. package/scripts/tests/test_rules.py +2 -83
  66. package/scripts/tests/test_tracked_private_terms.py +15 -0
  67. package/scripts/tests/test_verification_notice.py +29 -13
  68. package/.agents/skills/cert-classification-rule/SKILL.md +0 -224
  69. package/.agents/skills/cert-classification-rule/scripts/resort_cert_folders.py +0 -158
  70. package/.agents/skills/e-code-review/preflight-proposal.contract.test.mjs +0 -20
  71. package/.agents/skills/e-code-review/reference/effort-evaluation.md +0 -35
  72. package/.agents/skills/e-code-review/reference/loop.md +0 -261
  73. package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +0 -67
  74. package/.agents/skills/e-code-review/reference/preflight-proposal.md +0 -75
  75. package/.agents/skills/e-code-review/reference/runner-selection.md +0 -40
  76. package/.agents/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +0 -41
  77. package/.agents/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +0 -40
  78. package/.agents/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +0 -1
  79. package/.agents/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +0 -49
  80. package/.agents/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +0 -55
  81. package/.agents/skills/e-code-review/scripts/effort_defaults_evidence.json +0 -186
  82. package/.agents/skills/e-code-review/scripts/effort_evaluation.py +0 -362
  83. package/.agents/skills/e-code-review/scripts/finding_pipeline.py +0 -140
  84. package/.agents/skills/e-code-review/scripts/fixtures/demanding.json +0 -26
  85. package/.agents/skills/e-code-review/scripts/fixtures/easy.json +0 -14
  86. package/.agents/skills/e-code-review/scripts/fixtures/medium.json +0 -20
  87. package/.agents/skills/e-code-review/scripts/grok_code_review.py +0 -230
  88. package/.agents/skills/e-code-review/scripts/test_effort_evaluation.py +0 -180
  89. package/.agents/skills/e-code-review/scripts/test_finding_pipeline.py +0 -197
  90. package/.agents/skills/e-code-review/scripts/test_grok_code_review.py +0 -289
  91. package/_shared/pr-loop/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -82
  92. package/_shared/pr-loop/scripts/codex_usage_probe.py +0 -573
  93. package/_shared/pr-loop/scripts/test_codex_usage_probe.py +0 -683
  94. package/scripts/sync_to_cursor/AGENTS.md +0 -1
  95. package/scripts/sync_to_cursor/CLAUDE.md +0 -1
  96. package/scripts/sync_to_cursor/__init__.py +0 -13
  97. package/scripts/sync_to_cursor/canonical_docs.py +0 -66
  98. package/scripts/sync_to_cursor/config/__init__.py +0 -8
  99. package/scripts/sync_to_cursor/engine.py +0 -219
  100. package/scripts/sync_to_cursor/hashing.py +0 -7
  101. package/scripts/sync_to_cursor/paths.py +0 -18
  102. package/scripts/sync_to_cursor/rules.py +0 -494
  103. package/scripts/sync_to_cursor.py +0 -22
  104. package/scripts/tests/test_engine.py +0 -101
  105. package/scripts/tests/test_sync_to_cursor.py +0 -315
@@ -30,6 +30,7 @@ from advisor_scripts_constants.advisor_route_constants import (
30
30
  )
31
31
  from advisor_scripts_constants.astra_advisor_constants import (
32
32
  ADVISOR_CODEX_EXECUTABLE_ENV_VAR,
33
+ ALL_ACCOUNT_PICKER_RELATIVE_PARTS,
33
34
  ALL_ASTRA_TRUTHY_VALUES,
34
35
  ASTRA_BIND_FAILURE_REASON,
35
36
  ASTRA_CODEX_TIMEOUT_REASON,
@@ -41,10 +42,10 @@ from advisor_scripts_constants.astra_advisor_constants import (
41
42
  ASTRA_FALLBACK_KIND_BROKEN,
42
43
  ASTRA_FALLBACK_KIND_DECLINED,
43
44
  ASTRA_SESSION_ID_METAVAR,
44
- CLAUDE_CONFIG_DIRECTORY_NAME,
45
45
  CODEX_CONFIG_FLAG,
46
46
  CODEX_EXEC_SUBCOMMAND,
47
47
  CODEX_EXECUTABLE,
48
+ CODEX_HOME_ENV_VAR,
48
49
  CODEX_JSON_FLAG,
49
50
  CODEX_MODEL_FLAG,
50
51
  CODEX_PROMPT_FROM_STDIN,
@@ -52,10 +53,7 @@ from advisor_scripts_constants.astra_advisor_constants import (
52
53
  CODEX_REASONING_CONFIG_TEMPLATE,
53
54
  CODEX_RESUME_SUBCOMMAND,
54
55
  CODEX_SANDBOX_FLAG,
55
- USAGE_PROBE_FILENAME,
56
- USAGE_PROBE_PACKAGE_DIRECTORY_NAME,
57
- USAGE_PROBE_SCRIPTS_DIRECTORY_NAME,
58
- USAGE_PROBE_SHARED_DIRECTORY_NAME,
56
+ SHARED_PACKAGE_ROOT_PARENT_INDEX,
59
57
  )
60
58
  from codex_astra_preflight import AstraPreflight, run_astra_preflight
61
59
  from codex_astra_reply import (
@@ -176,23 +174,18 @@ def resolve_advisor_effort(all_settings: Mapping[str, str] | None) -> str:
176
174
  return resolve_advisor_pair(all_settings, None)[1]
177
175
 
178
176
 
179
- def resolve_usage_probe_path(home_directory: Path) -> Path:
180
- """Return the Codex usage-probe path under home_directory.
177
+ def resolve_account_picker_path() -> Path:
178
+ """Return the Codex account picker in the scripts directory beside this shared tree.
181
179
 
182
- Args:
183
- home_directory: Home directory that holds the installed probe.
180
+ ::
181
+
182
+ ~/.claude/_shared/advisor/scripts -> ~/.claude/scripts/codex_account_choice.py
184
183
 
185
184
  Returns:
186
- Path to the usage probe under that home.
185
+ Path to ``codex_account_choice.py``.
187
186
  """
188
- return (
189
- home_directory
190
- / CLAUDE_CONFIG_DIRECTORY_NAME
191
- / USAGE_PROBE_SHARED_DIRECTORY_NAME
192
- / USAGE_PROBE_PACKAGE_DIRECTORY_NAME
193
- / USAGE_PROBE_SCRIPTS_DIRECTORY_NAME
194
- / USAGE_PROBE_FILENAME
195
- )
187
+ shared_root = _scripts_directory.parents[SHARED_PACKAGE_ROOT_PARENT_INDEX]
188
+ return shared_root.joinpath(*ALL_ACCOUNT_PICKER_RELATIVE_PARTS)
196
189
 
197
190
 
198
191
  def resolve_codex_executable(all_settings: Mapping[str, str] | None) -> str | None:
@@ -244,21 +237,28 @@ def build_codex_arguments(
244
237
 
245
238
  def _resolve_preflight(
246
239
  preflight: AstraPreflight | None,
247
- probe_path: Path | None,
240
+ picker_path: Path | None,
248
241
  process_runner: Callable[..., subprocess.CompletedProcess[str]],
249
242
  ) -> AstraPreflight:
250
243
  if preflight is not None:
251
244
  return preflight
252
- resolved_path = resolve_usage_probe_path(Path.home()) if probe_path is None else probe_path
245
+ resolved_path = resolve_account_picker_path() if picker_path is None else picker_path
253
246
  return run_astra_preflight(resolved_path, process_runner)
254
247
 
255
248
 
249
+ def _codex_environment(codex_home: Path | None) -> dict[str, str] | None:
250
+ if codex_home is None:
251
+ return None
252
+ return {**os.environ, CODEX_HOME_ENV_VAR: str(codex_home)}
253
+
254
+
256
255
  def _run_codex(
257
256
  prompt: str,
258
257
  working_directory: Path,
259
258
  session_id: str | None,
260
259
  all_settings: Mapping[str, str] | None,
261
260
  executable: str,
261
+ codex_home: Path | None,
262
262
  process_runner: Callable[..., subprocess.CompletedProcess[str]],
263
263
  ) -> subprocess.CompletedProcess[str]:
264
264
  model_id, effort = resolve_advisor_pair(all_settings, None)
@@ -270,6 +270,7 @@ def _run_codex(
270
270
  model_id=model_id,
271
271
  ),
272
272
  cwd=str(working_directory),
273
+ env=_codex_environment(codex_home),
273
274
  input=prompt,
274
275
  capture_output=True,
275
276
  text=True,
@@ -292,7 +293,13 @@ def _run_enabled_advisor(
292
293
  return build_fallback_reply(resolved_preflight.reason, True, resolved_preflight.fallback_kind)
293
294
  try:
294
295
  completed = _run_codex(
295
- prompt, working_directory, session_id, setting_by_name, executable, process_runner
296
+ prompt,
297
+ working_directory,
298
+ session_id,
299
+ setting_by_name,
300
+ executable,
301
+ resolved_preflight.codex_home,
302
+ process_runner,
296
303
  )
297
304
  except subprocess.TimeoutExpired as error:
298
305
  return build_fallback_reply(
@@ -324,7 +331,7 @@ def run_codex_astra_advisor(
324
331
  prompt: str,
325
332
  working_directory: Path,
326
333
  preflight: AstraPreflight | None,
327
- probe_path: Path | None,
334
+ picker_path: Path | None,
328
335
  setting_by_name: Mapping[str, str] | None,
329
336
  session_id: str | None,
330
337
  process_runner: Callable[..., subprocess.CompletedProcess[str]],
@@ -334,11 +341,11 @@ def run_codex_astra_advisor(
334
341
  Args:
335
342
  prompt: Advisor prompt text.
336
343
  working_directory: Working directory for Codex.
337
- preflight: Usage probe result, or None to run the probe.
338
- probe_path: Usage-probe path, or None for the installed path.
344
+ preflight: Account picker result, or None to run the picker.
345
+ picker_path: Account picker path, or None for the one beside this shared tree.
339
346
  setting_by_name: Environment mapping, or None to read os.environ.
340
347
  session_id: Session to resume, or None for a new bind.
341
- process_runner: Callable that runs the probe and Codex.
348
+ process_runner: Callable that runs the picker and Codex.
342
349
 
343
350
  Returns:
344
351
  Advisor reply, or a fallback.
@@ -348,7 +355,7 @@ def run_codex_astra_advisor(
348
355
  executable = resolve_codex_executable(setting_by_name)
349
356
  if executable is None:
350
357
  return build_fallback_reply(ASTRA_EXECUTABLE_NOT_FOUND_REASON, True)
351
- resolved_preflight = _resolve_preflight(preflight, probe_path, process_runner)
358
+ resolved_preflight = _resolve_preflight(preflight, picker_path, process_runner)
352
359
  return _run_enabled_advisor(prompt, working_directory, setting_by_name, session_id, executable, resolved_preflight, process_runner)
353
360
 
354
361
 
@@ -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))
@@ -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"
@@ -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"