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
@@ -218,19 +218,19 @@ def test_non_object_evidence_reports_the_same_message_at_validate_and_load(
218
218
  def test_clean_single_spawn_at_top_of_slice_passes() -> None:
219
219
  run = ModelTierRun(
220
220
  own_tier="Opus",
221
- candidate_tiers=["Fable"],
222
- attempts=[{"tier": "Fable", "result": "spawned"}],
223
- selected_tier="Fable",
221
+ candidate_tiers=["Opus"],
222
+ attempts=[{"tier": "Opus", "result": "spawned"}],
223
+ selected_tier="Opus",
224
224
  )
225
225
  assert validate_model_tier_run(run) is None
226
226
 
227
227
 
228
- def test_astra_codex_bind_succeeds_after_fable_when_enabled() -> None:
228
+ def test_astra_codex_bind_succeeds_after_opus_when_enabled() -> None:
229
229
  run = ModelTierRun(
230
230
  own_tier="Opus",
231
- candidate_tiers=["Fable", ADVISOR_MODEL_TIER],
231
+ candidate_tiers=["Opus", ADVISOR_MODEL_TIER],
232
232
  attempts=[
233
- {"tier": "Fable", "result": "unavailable"},
233
+ {"tier": "Opus", "result": "unavailable"},
234
234
  {"tier": ADVISOR_MODEL_TIER, "result": CODEX_BIND_SUCCESS_TOKEN},
235
235
  ],
236
236
  selected_tier=ADVISOR_MODEL_TIER,
@@ -239,12 +239,12 @@ def test_astra_codex_bind_succeeds_after_fable_when_enabled() -> None:
239
239
  assert validate_model_tier_run(run) is None
240
240
 
241
241
 
242
- def test_fable_success_with_astra_enabled_stops_before_astra() -> None:
242
+ def test_opus_success_with_astra_enabled_stops_before_astra() -> None:
243
243
  run = ModelTierRun(
244
244
  own_tier="Opus",
245
- candidate_tiers=["Fable", ADVISOR_MODEL_TIER],
246
- attempts=[{"tier": "Fable", "result": "spawned"}],
247
- selected_tier="Fable",
245
+ candidate_tiers=["Opus", ADVISOR_MODEL_TIER],
246
+ attempts=[{"tier": "Opus", "result": "spawned"}],
247
+ selected_tier="Opus",
248
248
  is_astra_enabled=True,
249
249
  )
250
250
  assert validate_model_tier_run(run) is None
@@ -253,7 +253,7 @@ def test_fable_success_with_astra_enabled_stops_before_astra() -> None:
253
253
  def test_astra_first_walk_raises_when_astra_is_enabled() -> None:
254
254
  run = ModelTierRun(
255
255
  own_tier="Opus",
256
- candidate_tiers=[ADVISOR_MODEL_TIER, "Fable"],
256
+ candidate_tiers=[ADVISOR_MODEL_TIER, "Opus"],
257
257
  attempts=[{"tier": ADVISOR_MODEL_TIER, "result": CODEX_BIND_SUCCESS_TOKEN}],
258
258
  selected_tier=ADVISOR_MODEL_TIER,
259
259
  is_astra_enabled=True,
@@ -262,12 +262,12 @@ def test_astra_first_walk_raises_when_astra_is_enabled() -> None:
262
262
  validate_model_tier_run(run)
263
263
 
264
264
 
265
- def test_astra_rung_follows_fable_on_third_party_cli_floor_when_enabled() -> None:
265
+ def test_astra_rung_follows_opus_on_third_party_cli_floor_when_enabled() -> None:
266
266
  run = ModelTierRun(
267
267
  own_tier="ThirdParty",
268
- candidate_tiers=["Fable", ADVISOR_MODEL_TIER],
268
+ candidate_tiers=["Opus", ADVISOR_MODEL_TIER],
269
269
  attempts=[
270
- {"tier": "Fable", "result": "unavailable"},
270
+ {"tier": "Opus", "result": "unavailable"},
271
271
  {"tier": ADVISOR_MODEL_TIER, "result": CODEX_BIND_SUCCESS_TOKEN},
272
272
  ],
273
273
  selected_tier=ADVISOR_MODEL_TIER,
@@ -281,9 +281,9 @@ def test_astra_rung_follows_fable_on_third_party_cli_floor_when_enabled() -> Non
281
281
  def test_astra_codex_result_requires_astra_candidate() -> None:
282
282
  run = ModelTierRun(
283
283
  own_tier="Opus",
284
- candidate_tiers=["Fable"],
285
- attempts=[{"tier": "Fable", "result": CODEX_BIND_SUCCESS_TOKEN}],
286
- selected_tier="Fable",
284
+ candidate_tiers=["Opus"],
285
+ attempts=[{"tier": "Opus", "result": CODEX_BIND_SUCCESS_TOKEN}],
286
+ selected_tier="Opus",
287
287
  )
288
288
  with pytest.raises(ModelTierRunError):
289
289
  validate_model_tier_run(run)
@@ -292,9 +292,9 @@ def test_astra_codex_result_requires_astra_candidate() -> None:
292
292
  def test_astra_spawned_result_does_not_count_as_codex_success() -> None:
293
293
  run = ModelTierRun(
294
294
  own_tier="Opus",
295
- candidate_tiers=["Fable", ADVISOR_MODEL_TIER],
295
+ candidate_tiers=["Opus", ADVISOR_MODEL_TIER],
296
296
  attempts=[
297
- {"tier": "Fable", "result": "unavailable"},
297
+ {"tier": "Opus", "result": "unavailable"},
298
298
  {"tier": ADVISOR_MODEL_TIER, "result": "spawned"},
299
299
  ],
300
300
  selected_tier=ADVISOR_MODEL_TIER,
@@ -304,13 +304,13 @@ def test_astra_spawned_result_does_not_count_as_codex_success() -> None:
304
304
  validate_model_tier_run(run)
305
305
 
306
306
 
307
- def test_exhausted_fable_walk_fails_closed() -> None:
307
+ def test_exhausted_opus_walk_fails_closed() -> None:
308
308
  run = ModelTierRun(
309
309
  own_tier="Opus",
310
- candidate_tiers=["Fable"],
311
- attempts=[{"tier": "Fable", "result": "unavailable"}],
310
+ candidate_tiers=["Opus"],
311
+ attempts=[{"tier": "Opus", "result": "unavailable"}],
312
312
  selected_tier=None,
313
- fallback_reason="Fable did not bind; no advisor",
313
+ fallback_reason="Opus did not bind; no advisor",
314
314
  )
315
315
  assert validate_model_tier_run(run) is None
316
316
 
@@ -318,8 +318,8 @@ def test_exhausted_fable_walk_fails_closed() -> None:
318
318
  def test_fully_exhausted_walk_with_fallback_reason_passes() -> None:
319
319
  run = ModelTierRun(
320
320
  own_tier="Opus",
321
- candidate_tiers=["Fable"],
322
- attempts=[{"tier": "Fable", "result": "unavailable"}],
321
+ candidate_tiers=["Opus"],
322
+ attempts=[{"tier": "Opus", "result": "unavailable"}],
323
323
  selected_tier=None,
324
324
  fallback_reason="every candidate tier failed",
325
325
  )
@@ -329,9 +329,9 @@ def test_fully_exhausted_walk_with_fallback_reason_passes() -> None:
329
329
  def test_opus_candidate_on_advisor_walk_raises() -> None:
330
330
  run = ModelTierRun(
331
331
  own_tier="Opus",
332
- candidate_tiers=["Fable", "Opus"],
333
- attempts=[{"tier": "Fable", "result": "spawned"}],
334
- selected_tier="Fable",
332
+ candidate_tiers=["Opus", "Opus"],
333
+ attempts=[{"tier": "Opus", "result": "spawned"}],
334
+ selected_tier="Opus",
335
335
  )
336
336
  with pytest.raises(ModelTierRunError):
337
337
  validate_model_tier_run(run)
@@ -340,7 +340,7 @@ def test_opus_candidate_on_advisor_walk_raises() -> None:
340
340
  def test_attempt_tier_outside_candidate_slice_raises() -> None:
341
341
  run = ModelTierRun(
342
342
  own_tier="Opus",
343
- candidate_tiers=["Fable"],
343
+ candidate_tiers=["Opus"],
344
344
  attempts=[{"tier": "Haiku", "result": "spawned"}],
345
345
  selected_tier="Haiku",
346
346
  )
@@ -351,10 +351,10 @@ def test_attempt_tier_outside_candidate_slice_raises() -> None:
351
351
  def test_attempts_out_of_ladder_order_raises() -> None:
352
352
  run = ModelTierRun(
353
353
  own_tier="Opus",
354
- candidate_tiers=["Fable", ADVISOR_MODEL_TIER],
354
+ candidate_tiers=["Opus", ADVISOR_MODEL_TIER],
355
355
  attempts=[
356
356
  {"tier": ADVISOR_MODEL_TIER, "result": CODEX_BIND_SUCCESS_TOKEN},
357
- {"tier": "Fable", "result": "unavailable"},
357
+ {"tier": "Opus", "result": "unavailable"},
358
358
  ],
359
359
  selected_tier=ADVISOR_MODEL_TIER,
360
360
  is_astra_enabled=True,
@@ -366,12 +366,12 @@ def test_attempts_out_of_ladder_order_raises() -> None:
366
366
  def test_selected_tier_not_first_spawned_attempt_raises() -> None:
367
367
  run = ModelTierRun(
368
368
  own_tier="Opus",
369
- candidate_tiers=["Fable", ADVISOR_MODEL_TIER],
369
+ candidate_tiers=["Opus", ADVISOR_MODEL_TIER],
370
370
  attempts=[
371
- {"tier": "Fable", "result": "unavailable"},
371
+ {"tier": "Opus", "result": "unavailable"},
372
372
  {"tier": ADVISOR_MODEL_TIER, "result": CODEX_BIND_SUCCESS_TOKEN},
373
373
  ],
374
- selected_tier="Fable",
374
+ selected_tier="Opus",
375
375
  is_astra_enabled=True,
376
376
  )
377
377
  with pytest.raises(
@@ -387,9 +387,9 @@ def test_selected_tier_not_first_spawned_attempt_raises() -> None:
387
387
  def test_exhausted_walk_with_non_null_selected_tier_raises() -> None:
388
388
  run = ModelTierRun(
389
389
  own_tier="Opus",
390
- candidate_tiers=["Fable"],
391
- attempts=[{"tier": "Fable", "result": "unavailable"}],
392
- selected_tier="Fable",
390
+ candidate_tiers=["Opus"],
391
+ attempts=[{"tier": "Opus", "result": "unavailable"}],
392
+ selected_tier="Opus",
393
393
  fallback_reason="every candidate tier failed",
394
394
  )
395
395
  with pytest.raises(ModelTierRunError):
@@ -399,8 +399,8 @@ def test_exhausted_walk_with_non_null_selected_tier_raises() -> None:
399
399
  def test_exhausted_walk_missing_fallback_reason_raises() -> None:
400
400
  run = ModelTierRun(
401
401
  own_tier="Opus",
402
- candidate_tiers=["Fable"],
403
- attempts=[{"tier": "Fable", "result": "unavailable"}],
402
+ candidate_tiers=["Opus"],
403
+ attempts=[{"tier": "Opus", "result": "unavailable"}],
404
404
  selected_tier=None,
405
405
  )
406
406
  with pytest.raises(ModelTierRunError):
@@ -421,7 +421,7 @@ def test_unknown_own_tier_raises() -> None:
421
421
  def test_empty_attempts_with_null_selected_tier_raises() -> None:
422
422
  run = ModelTierRun(
423
423
  own_tier="Opus",
424
- candidate_tiers=["Fable"],
424
+ candidate_tiers=["Opus"],
425
425
  attempts=[],
426
426
  selected_tier=None,
427
427
  fallback_reason="skipped straight to CLI fallback",
@@ -433,10 +433,10 @@ def test_empty_attempts_with_null_selected_tier_raises() -> None:
433
433
  def test_incomplete_fallback_walk_before_astra_raises() -> None:
434
434
  run = ModelTierRun(
435
435
  own_tier="Opus",
436
- candidate_tiers=["Fable", ADVISOR_MODEL_TIER],
437
- attempts=[{"tier": "Fable", "result": "unavailable"}],
436
+ candidate_tiers=["Opus", ADVISOR_MODEL_TIER],
437
+ attempts=[{"tier": "Opus", "result": "unavailable"}],
438
438
  selected_tier=None,
439
- fallback_reason="stopped after Fable without trying Astra",
439
+ fallback_reason="stopped after Opus without trying Astra",
440
440
  is_astra_enabled=True,
441
441
  )
442
442
  with pytest.raises(ModelTierRunError):
@@ -446,9 +446,9 @@ def test_incomplete_fallback_walk_before_astra_raises() -> None:
446
446
  def test_lowercase_own_tier_and_candidates_pass() -> None:
447
447
  run = ModelTierRun(
448
448
  own_tier="opus",
449
- candidate_tiers=["fable"],
450
- attempts=[{"tier": "fable", "result": "spawned"}],
451
- selected_tier="fable",
449
+ candidate_tiers=["opus"],
450
+ attempts=[{"tier": "opus", "result": "spawned"}],
451
+ selected_tier="opus",
452
452
  )
453
453
  assert validate_model_tier_run(run) is None
454
454
 
@@ -459,16 +459,16 @@ def test_cli_validates_json_log_file(tmp_path: Path) -> None:
459
459
  json.dumps(
460
460
  {
461
461
  "own_tier": "Opus",
462
- "candidate_tiers": ["Fable"],
463
- "attempts": [{"tier": "Fable", "result": "spawned"}],
464
- "selected_tier": "Fable",
462
+ "candidate_tiers": ["Opus"],
463
+ "attempts": [{"tier": "Opus", "result": "spawned"}],
464
+ "selected_tier": "Opus",
465
465
  }
466
466
  ),
467
467
  encoding="utf-8",
468
468
  )
469
469
  assert main([str(log_path)]) == 0
470
470
  loaded_run = load_model_tier_run_from_json_path(from_path=log_path)
471
- assert loaded_run.selected_tier == "Fable"
471
+ assert loaded_run.selected_tier == "Opus"
472
472
  assert loaded_run.host_profile == "Claude"
473
473
 
474
474
 
@@ -478,9 +478,9 @@ def test_cli_rejects_non_string_host_profile(tmp_path: Path) -> None:
478
478
  json.dumps(
479
479
  {
480
480
  "own_tier": "Opus",
481
- "candidate_tiers": ["Fable"],
482
- "attempts": [{"tier": "Fable", "result": "spawned"}],
483
- "selected_tier": "Fable",
481
+ "candidate_tiers": ["Opus"],
482
+ "attempts": [{"tier": "Opus", "result": "spawned"}],
483
+ "selected_tier": "Opus",
484
484
  "host_profile": 1,
485
485
  }
486
486
  ),
@@ -495,9 +495,9 @@ def test_cli_rejects_non_boolean_astra_enabled(tmp_path: Path) -> None:
495
495
  json.dumps(
496
496
  {
497
497
  "own_tier": "Opus",
498
- "candidate_tiers": ["Fable"],
499
- "attempts": [{"tier": "Fable", "result": "spawned"}],
500
- "selected_tier": "Fable",
498
+ "candidate_tiers": ["Opus"],
499
+ "attempts": [{"tier": "Opus", "result": "spawned"}],
500
+ "selected_tier": "Opus",
501
501
  "astra_enabled": "false",
502
502
  }
503
503
  ),
@@ -512,8 +512,8 @@ def test_cli_rejects_incomplete_fallback_log(tmp_path: Path) -> None:
512
512
  json.dumps(
513
513
  {
514
514
  "own_tier": "Opus",
515
- "candidate_tiers": ["Fable", "Astra"],
516
- "attempts": [{"tier": "Fable", "result": "unavailable"}],
515
+ "candidate_tiers": ["Opus", "Astra"],
516
+ "attempts": [{"tier": "Opus", "result": "unavailable"}],
517
517
  "selected_tier": None,
518
518
  "fallback_reason": "incomplete",
519
519
  "astra_enabled": True,
@@ -531,9 +531,9 @@ def test_cli_loads_astra_enabled_fallback_walk(tmp_path: Path) -> None:
531
531
  {
532
532
  "own_tier": "ThirdParty",
533
533
  "host_profile": "ThirdParty",
534
- "candidate_tiers": ["Fable", "Astra"],
534
+ "candidate_tiers": ["Opus", "Astra"],
535
535
  "attempts": [
536
- {"tier": "Fable", "result": "unavailable"},
536
+ {"tier": "Opus", "result": "unavailable"},
537
537
  {"tier": "Astra", "result": "codex"},
538
538
  ],
539
539
  "selected_tier": "Astra",
@@ -552,12 +552,12 @@ def test_cli_missing_path_returns_usage_exit_code() -> None:
552
552
  assert main([]) == 2
553
553
 
554
554
 
555
- def test_cli_bind_at_fable_passes() -> None:
555
+ def test_cli_bind_at_opus_passes() -> None:
556
556
  run = ModelTierRun(
557
557
  own_tier="Opus",
558
- candidate_tiers=["Fable"],
559
- attempts=[{"tier": "Fable", "result": "cli"}],
560
- selected_tier="Fable",
558
+ candidate_tiers=["Opus"],
559
+ attempts=[{"tier": "Opus", "result": "cli"}],
560
+ selected_tier="Opus",
561
561
  )
562
562
  assert validate_model_tier_run(run) is None
563
563
 
@@ -565,9 +565,9 @@ def test_cli_bind_at_fable_passes() -> None:
565
565
  def test_cli_bind_fallthrough_to_opus_raises() -> None:
566
566
  run = ModelTierRun(
567
567
  own_tier="Opus",
568
- candidate_tiers=["Fable", "Opus"],
568
+ candidate_tiers=["Opus", "Opus"],
569
569
  attempts=[
570
- {"tier": "Fable", "result": "unavailable"},
570
+ {"tier": "Opus", "result": "unavailable"},
571
571
  {"tier": "Opus", "result": "cli"},
572
572
  ],
573
573
  selected_tier="Opus",
@@ -576,12 +576,12 @@ def test_cli_bind_fallthrough_to_opus_raises() -> None:
576
576
  validate_model_tier_run(run)
577
577
 
578
578
 
579
- def test_third_party_own_tier_maps_to_fable_cli_bind_passes() -> None:
579
+ def test_third_party_own_tier_maps_to_opus_cli_bind_passes() -> None:
580
580
  run = ModelTierRun(
581
581
  own_tier="ThirdParty",
582
- candidate_tiers=["Fable"],
583
- attempts=[{"tier": "Fable", "result": "cli"}],
584
- selected_tier="Fable",
582
+ candidate_tiers=["Opus"],
583
+ attempts=[{"tier": "Opus", "result": "cli"}],
584
+ selected_tier="Opus",
585
585
  )
586
586
  assert validate_model_tier_run(run) is None
587
587
 
@@ -589,9 +589,9 @@ def test_third_party_own_tier_maps_to_fable_cli_bind_passes() -> None:
589
589
  def test_third_party_own_tier_lowercase_cli_bind_passes() -> None:
590
590
  run = ModelTierRun(
591
591
  own_tier="thirdparty",
592
- candidate_tiers=["fable"],
593
- attempts=[{"tier": "fable", "result": "cli"}],
594
- selected_tier="fable",
592
+ candidate_tiers=["opus"],
593
+ attempts=[{"tier": "opus", "result": "cli"}],
594
+ selected_tier="opus",
595
595
  )
596
596
  assert validate_model_tier_run(run) is None
597
597
 
@@ -599,9 +599,9 @@ def test_third_party_own_tier_lowercase_cli_bind_passes() -> None:
599
599
  def test_self_token_is_not_bind_success_raises() -> None:
600
600
  run = ModelTierRun(
601
601
  own_tier="Opus",
602
- candidate_tiers=["Fable"],
603
- attempts=[{"tier": "Fable", "result": "self"}],
604
- selected_tier="Fable",
602
+ candidate_tiers=["Opus"],
603
+ attempts=[{"tier": "Opus", "result": "self"}],
604
+ selected_tier="Opus",
605
605
  )
606
606
  with pytest.raises(ModelTierRunError):
607
607
  validate_model_tier_run(run)
@@ -610,9 +610,9 @@ def test_self_token_is_not_bind_success_raises() -> None:
610
610
  def test_third_party_self_token_is_not_bind_success_raises() -> None:
611
611
  run = ModelTierRun(
612
612
  own_tier="ThirdParty",
613
- candidate_tiers=["Fable"],
614
- attempts=[{"tier": "Fable", "result": "self"}],
615
- selected_tier="Fable",
613
+ candidate_tiers=["Opus"],
614
+ attempts=[{"tier": "Opus", "result": "self"}],
615
+ selected_tier="Opus",
616
616
  )
617
617
  with pytest.raises(ModelTierRunError):
618
618
  validate_model_tier_run(run)
@@ -632,8 +632,8 @@ def test_third_party_host_legacy_single_tier_self_bind_raises() -> None:
632
632
  def test_third_party_cli_exhausted_fail_closed_passes() -> None:
633
633
  run = ModelTierRun(
634
634
  own_tier="ThirdParty",
635
- candidate_tiers=["Fable"],
636
- attempts=[{"tier": "Fable", "result": "unavailable"}],
635
+ candidate_tiers=["Opus"],
636
+ attempts=[{"tier": "Opus", "result": "unavailable"}],
637
637
  selected_tier=None,
638
638
  fallback_reason=(
639
639
  "third-party host CLI Claude-chain exhausted; fail closed"
@@ -645,8 +645,8 @@ def test_third_party_cli_exhausted_fail_closed_passes() -> None:
645
645
  def test_third_party_cli_exhausted_without_fallback_reason_raises() -> None:
646
646
  run = ModelTierRun(
647
647
  own_tier="ThirdParty",
648
- candidate_tiers=["Fable"],
649
- attempts=[{"tier": "Fable", "result": "unavailable"}],
648
+ candidate_tiers=["Opus"],
649
+ attempts=[{"tier": "Opus", "result": "unavailable"}],
650
650
  selected_tier=None,
651
651
  )
652
652
  with pytest.raises(ModelTierRunError):
@@ -656,9 +656,9 @@ def test_third_party_cli_exhausted_without_fallback_reason_raises() -> None:
656
656
  def test_third_party_cli_selected_tier_mismatch_raises() -> None:
657
657
  run = ModelTierRun(
658
658
  own_tier="ThirdParty",
659
- candidate_tiers=["Fable"],
660
- attempts=[{"tier": "Fable", "result": "cli"}],
661
- selected_tier="Opus",
659
+ candidate_tiers=["Opus"],
660
+ attempts=[{"tier": "Opus", "result": "cli"}],
661
+ selected_tier="Sonnet",
662
662
  )
663
663
  with pytest.raises(ModelTierRunError):
664
664
  validate_model_tier_run(run)
@@ -667,9 +667,9 @@ def test_third_party_cli_selected_tier_mismatch_raises() -> None:
667
667
  def test_claude_host_self_token_is_not_spawn_success_raises() -> None:
668
668
  run = ModelTierRun(
669
669
  own_tier="Opus",
670
- candidate_tiers=["Fable"],
671
- attempts=[{"tier": "Fable", "result": "self"}],
672
- selected_tier="Fable",
670
+ candidate_tiers=["Opus"],
671
+ attempts=[{"tier": "Opus", "result": "self"}],
672
+ selected_tier="Opus",
673
673
  )
674
674
  with pytest.raises(ModelTierRunError):
675
675
  validate_model_tier_run(run)
@@ -699,10 +699,10 @@ def test_codex_host_astra_codex_token_counts_as_success() -> None:
699
699
  assert validate_model_tier_run(run) is None
700
700
 
701
701
 
702
- def test_codex_host_fable_then_astra_walk_raises() -> None:
702
+ def test_codex_host_opus_then_astra_walk_raises() -> None:
703
703
  run = ModelTierRun(
704
704
  own_tier="Opus",
705
- candidate_tiers=["Fable", ADVISOR_MODEL_TIER],
705
+ candidate_tiers=["Opus", ADVISOR_MODEL_TIER],
706
706
  attempts=[{"tier": ADVISOR_MODEL_TIER, "result": "spawned"}],
707
707
  selected_tier=ADVISOR_MODEL_TIER,
708
708
  is_astra_enabled=True,
@@ -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"