claude-dev-env 8.38.2 → 8.39.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.
- package/_shared/advisor/reference/astra-rung.md +7 -7
- package/_shared/advisor/scripts/codex_astra_advisor.py +13 -13
- package/_shared/advisor/scripts/codex_astra_preflight.py +62 -33
- package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +5 -2
- package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +64 -33
- package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +125 -3
- package/_shared/pr-loop/scripts/check_convergence.py +23 -17
- package/_shared/pr-loop/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +4 -3
- package/_shared/pr-loop/scripts/test_check_convergence_codex.py +64 -25
- package/docs/codex-accounts.md +27 -31
- package/hooks/session/skill_loaded_reminder.py +54 -25
- package/hooks/session/test_skill_loaded_reminder.py +94 -12
- package/package.json +1 -1
- package/scripts/codex_account_choice.py +8 -303
- package/scripts/test_codex_account_choice.py +11 -273
|
@@ -1,27 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""Keep each Codex account's home and launcher ready.
|
|
3
3
|
|
|
4
4
|
Each Codex account signs in under its own Codex home in
|
|
5
5
|
``~/.codex-profiles/<name>``. The account roster names them in try order: the
|
|
6
6
|
``CODEX_ACCOUNT_PROFILES`` environment variable, comma-separated, else the saved
|
|
7
7
|
``account-launchers.json`` list under the profiles root. With neither,
|
|
8
|
-
``codex-1`` through ``codex-4`` are the roster.
|
|
8
|
+
``codex-1`` through ``codex-4`` are the roster.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
none over 10%, the roomiest one over 1% -> luna, stop at 1% left
|
|
12
|
-
... but an account with a 5-hour window needs 20% of it left for luna
|
|
13
|
-
none over 1%, or no meter reads -> wait, name the first reset
|
|
14
|
-
account not signed in, or its meter unread -> skipped
|
|
15
|
-
|
|
16
|
-
Five commands::
|
|
17
|
-
|
|
18
|
-
python codex_account_choice.py choose
|
|
19
|
-
{"tier": "normal", "account": "codex-1", "codex_home": "C:/Users/me/.codex-profiles/codex-1",
|
|
20
|
-
"percent_left": 62.0, "stop_below_percent": null, "reason": "codex-1 has 62% left",
|
|
21
|
-
"accounts": [...]}
|
|
22
|
-
|
|
23
|
-
python codex_account_choice.py check codex-2 --floor 1
|
|
24
|
-
exit 0 while codex-2 has more than 1% left, exit 3 once it has not
|
|
10
|
+
Three commands::
|
|
25
11
|
|
|
26
12
|
python codex_account_choice.py sync
|
|
27
13
|
links the shared Codex setup (config, rules, skills, plugins, agents) into
|
|
@@ -42,7 +28,6 @@ import json
|
|
|
42
28
|
import os
|
|
43
29
|
import sys
|
|
44
30
|
from collections.abc import Callable, Sequence
|
|
45
|
-
from dataclasses import dataclass
|
|
46
31
|
from datetime import datetime, timezone
|
|
47
32
|
from pathlib import Path
|
|
48
33
|
|
|
@@ -54,12 +39,6 @@ from claude_account_profile import (
|
|
|
54
39
|
validate_profile_name,
|
|
55
40
|
write_launcher,
|
|
56
41
|
)
|
|
57
|
-
from codex_account_meters import (
|
|
58
|
-
CodexAccountMeters,
|
|
59
|
-
CodexMeterUnreadError,
|
|
60
|
-
read_codex_meters,
|
|
61
|
-
resolve_codex_path,
|
|
62
|
-
)
|
|
63
42
|
from dev_env_scripts_constants.claude_account_constants import (
|
|
64
43
|
ALL_LAUNCHER_DIRECTORY_RELATIVE_PARTS,
|
|
65
44
|
JSON_LAUNCHER_KEY,
|
|
@@ -70,25 +49,15 @@ from dev_env_scripts_constants.codex_account_constants import (
|
|
|
70
49
|
CODEX_ACCOUNT_LAUNCHERS_FILE_NAME,
|
|
71
50
|
CODEX_ACCOUNT_NAME_SEPARATOR,
|
|
72
51
|
CODEX_ACCOUNT_PROFILES_ENVIRONMENT_VARIABLE,
|
|
73
|
-
CODEX_AUTH_FILE_NAME,
|
|
74
52
|
CODEX_LAUNCHER_PROGRAM,
|
|
75
53
|
CODEX_PROFILES_ROOT_DIRECTORY_NAME,
|
|
76
54
|
CODEX_PROFILES_ROOT_ENVIRONMENT_VARIABLE,
|
|
77
55
|
COMMAND_INSTALL,
|
|
78
56
|
COMMAND_SETUP,
|
|
79
|
-
EXIT_CODE_NO_ROOM,
|
|
80
|
-
EXIT_CODE_ROOM,
|
|
81
57
|
INVALID_ACCOUNT_ROSTER_TEMPLATE,
|
|
82
58
|
JSON_INSTALLED_KEY,
|
|
83
59
|
JSON_RETIRED_KEY,
|
|
84
|
-
LUNA_TIER_SHORT_WINDOW_MINIMUM_PERCENT_LEFT,
|
|
85
|
-
LUNA_TIER_STOP_PERCENT_LEFT,
|
|
86
60
|
MAIN_CODEX_HOME_DIRECTORY_NAME,
|
|
87
|
-
NORMAL_TIER_MINIMUM_PERCENT_LEFT,
|
|
88
|
-
REASON_LUNA_TEMPLATE,
|
|
89
|
-
REASON_NORMAL_TEMPLATE,
|
|
90
|
-
REASON_WAIT_TEMPLATE,
|
|
91
|
-
REASON_WAIT_UNREAD,
|
|
92
61
|
ROSTER_JSON_INDENT,
|
|
93
62
|
ROSTER_NOT_A_LIST_TEMPLATE,
|
|
94
63
|
SETUP_NAMES_SEPARATOR,
|
|
@@ -96,41 +65,14 @@ from dev_env_scripts_constants.codex_account_constants import (
|
|
|
96
65
|
SETUP_PROMPT_TEXT,
|
|
97
66
|
SETUP_SAVED_NAMES_TEMPLATE,
|
|
98
67
|
TEXT_ENCODING,
|
|
99
|
-
TIER_LUNA,
|
|
100
|
-
TIER_NORMAL,
|
|
101
|
-
TIER_WAIT,
|
|
102
|
-
UNKNOWN_ACCOUNT_TEMPLATE,
|
|
103
|
-
UNKNOWN_RESET_TEXT,
|
|
104
|
-
UNREAD_NOT_SIGNED_IN,
|
|
105
68
|
)
|
|
106
69
|
|
|
107
|
-
MeterReader = Callable[[Path], CodexAccountMeters]
|
|
108
70
|
LineReader = Callable[[str], str]
|
|
109
71
|
LineWriter = Callable[[str], None]
|
|
110
72
|
|
|
111
73
|
|
|
112
74
|
class CodexAccountNameError(ValueError):
|
|
113
|
-
"""An account name from the roster
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
@dataclass(frozen=True)
|
|
117
|
-
class AccountReading:
|
|
118
|
-
"""One account's meters, or why they could not be read."""
|
|
119
|
-
|
|
120
|
-
name: str
|
|
121
|
-
codex_home: Path
|
|
122
|
-
meters: CodexAccountMeters | None
|
|
123
|
-
unread_reason: str | None = None
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
@dataclass(frozen=True)
|
|
127
|
-
class CodexAccountDecision:
|
|
128
|
-
"""The tier and account a job runs on, and the plain-words reason."""
|
|
129
|
-
|
|
130
|
-
tier: str
|
|
131
|
-
reading: AccountReading | None
|
|
132
|
-
stop_below_percent: float | None
|
|
133
|
-
reason: str
|
|
75
|
+
"""An account name from the roster is invalid or unknown."""
|
|
134
76
|
|
|
135
77
|
|
|
136
78
|
def default_profiles_root() -> Path:
|
|
@@ -236,7 +178,7 @@ def saved_codex_account_names(profiles_root: Path) -> tuple[str, ...]:
|
|
|
236
178
|
|
|
237
179
|
|
|
238
180
|
def codex_account_names(profiles_root: Path) -> tuple[str, ...]:
|
|
239
|
-
"""Name the accounts
|
|
181
|
+
"""Name the accounts the broker and setup commands use, in try order.
|
|
240
182
|
|
|
241
183
|
Args:
|
|
242
184
|
profiles_root: The directory that holds every account's Codex home.
|
|
@@ -418,234 +360,6 @@ def update_codex_account_roster(
|
|
|
418
360
|
return all_names, all_retired
|
|
419
361
|
|
|
420
362
|
|
|
421
|
-
def read_account(
|
|
422
|
-
name: str, profiles_root: Path, read_meters: MeterReader
|
|
423
|
-
) -> AccountReading:
|
|
424
|
-
"""Read one account's meters from its Codex home.
|
|
425
|
-
|
|
426
|
-
Args:
|
|
427
|
-
name: The account name.
|
|
428
|
-
profiles_root: The directory that holds every account's Codex home.
|
|
429
|
-
read_meters: Reads the meters under one Codex home.
|
|
430
|
-
|
|
431
|
-
Returns:
|
|
432
|
-
The meters, or the reason they are unread.
|
|
433
|
-
"""
|
|
434
|
-
codex_home = profiles_root / name
|
|
435
|
-
if not (codex_home / CODEX_AUTH_FILE_NAME).is_file():
|
|
436
|
-
return AccountReading(name, codex_home, None, UNREAD_NOT_SIGNED_IN)
|
|
437
|
-
try:
|
|
438
|
-
return AccountReading(name, codex_home, read_meters(codex_home))
|
|
439
|
-
except CodexMeterUnreadError as error:
|
|
440
|
-
return AccountReading(name, codex_home, None, str(error))
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
def _first_reset(
|
|
444
|
-
all_read: Sequence[AccountReading],
|
|
445
|
-
) -> tuple[datetime, AccountReading] | None:
|
|
446
|
-
all_resets = [
|
|
447
|
-
(each_reset, each_reading)
|
|
448
|
-
for each_reading in all_read
|
|
449
|
-
if each_reading.meters is not None
|
|
450
|
-
and (
|
|
451
|
-
each_reset := each_reading.meters.resets_before_room(
|
|
452
|
-
NORMAL_TIER_MINIMUM_PERCENT_LEFT
|
|
453
|
-
)
|
|
454
|
-
)
|
|
455
|
-
is not None
|
|
456
|
-
]
|
|
457
|
-
if not all_resets:
|
|
458
|
-
return None
|
|
459
|
-
return min(all_resets, key=lambda each_pair: each_pair[0])
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
def _wait_decision(all_read: Sequence[AccountReading]) -> CodexAccountDecision:
|
|
463
|
-
if not all_read:
|
|
464
|
-
return CodexAccountDecision(TIER_WAIT, None, None, REASON_WAIT_UNREAD)
|
|
465
|
-
first_reset = _first_reset(all_read)
|
|
466
|
-
if first_reset is None:
|
|
467
|
-
reset_text, first_reading = UNKNOWN_RESET_TEXT, all_read[0]
|
|
468
|
-
else:
|
|
469
|
-
reset_text, first_reading = first_reset[0].isoformat(), first_reset[1]
|
|
470
|
-
return CodexAccountDecision(
|
|
471
|
-
TIER_WAIT,
|
|
472
|
-
first_reading,
|
|
473
|
-
None,
|
|
474
|
-
REASON_WAIT_TEMPLATE.format(account=first_reading.name, reset=reset_text),
|
|
475
|
-
)
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
def _percent_left(reading: AccountReading) -> float:
|
|
479
|
-
return reading.meters.percent_left if reading.meters is not None else 0.0
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
def _normal_decision(all_read: Sequence[AccountReading]) -> CodexAccountDecision | None:
|
|
483
|
-
for each_reading in all_read:
|
|
484
|
-
percent_left = _percent_left(each_reading)
|
|
485
|
-
if percent_left > NORMAL_TIER_MINIMUM_PERCENT_LEFT:
|
|
486
|
-
return CodexAccountDecision(
|
|
487
|
-
TIER_NORMAL,
|
|
488
|
-
each_reading,
|
|
489
|
-
None,
|
|
490
|
-
REASON_NORMAL_TEMPLATE.format(
|
|
491
|
-
account=each_reading.name, percent_left=percent_left
|
|
492
|
-
),
|
|
493
|
-
)
|
|
494
|
-
return None
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
def _can_run_luna(reading: AccountReading) -> bool:
|
|
498
|
-
if reading.meters is None or reading.meters.percent_left <= LUNA_TIER_STOP_PERCENT_LEFT:
|
|
499
|
-
return False
|
|
500
|
-
short_window_left = reading.meters.short_window_percent_left
|
|
501
|
-
return (
|
|
502
|
-
short_window_left is None
|
|
503
|
-
or short_window_left >= LUNA_TIER_SHORT_WINDOW_MINIMUM_PERCENT_LEFT
|
|
504
|
-
)
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
def _luna_decision(all_read: Sequence[AccountReading]) -> CodexAccountDecision | None:
|
|
508
|
-
all_candidates = [each_reading for each_reading in all_read if _can_run_luna(each_reading)]
|
|
509
|
-
if not all_candidates:
|
|
510
|
-
return None
|
|
511
|
-
roomiest = max(all_candidates, key=_percent_left)
|
|
512
|
-
return CodexAccountDecision(
|
|
513
|
-
TIER_LUNA,
|
|
514
|
-
roomiest,
|
|
515
|
-
LUNA_TIER_STOP_PERCENT_LEFT,
|
|
516
|
-
REASON_LUNA_TEMPLATE.format(
|
|
517
|
-
bar=NORMAL_TIER_MINIMUM_PERCENT_LEFT,
|
|
518
|
-
account=roomiest.name,
|
|
519
|
-
percent_left=_percent_left(roomiest),
|
|
520
|
-
stop=LUNA_TIER_STOP_PERCENT_LEFT,
|
|
521
|
-
),
|
|
522
|
-
)
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
def choose_codex_account(all_readings: Sequence[AccountReading]) -> CodexAccountDecision:
|
|
526
|
-
"""Pick the tier and account a job runs on.
|
|
527
|
-
|
|
528
|
-
::
|
|
529
|
-
|
|
530
|
-
codex-1 8%, codex-2 40% -> normal on codex-2
|
|
531
|
-
codex-1 8%, codex-2 3%, others 0 -> luna on codex-1, stop at 1%
|
|
532
|
-
codex-1 week 8%, 5-hour 15% left -> no luna on codex-1, 5-hour under 20%
|
|
533
|
-
every account 1% or less -> wait, naming the account that resets first
|
|
534
|
-
codex-1 unread, codex-2 40% -> normal on codex-2
|
|
535
|
-
|
|
536
|
-
Args:
|
|
537
|
-
all_readings: Every account's reading, in try order.
|
|
538
|
-
|
|
539
|
-
Returns:
|
|
540
|
-
The tier, the account, where a Luna run stops, and the reason.
|
|
541
|
-
"""
|
|
542
|
-
all_read = [
|
|
543
|
-
each_reading for each_reading in all_readings if each_reading.meters is not None
|
|
544
|
-
]
|
|
545
|
-
return (
|
|
546
|
-
_normal_decision(all_read)
|
|
547
|
-
or _luna_decision(all_read)
|
|
548
|
-
or _wait_decision(all_read)
|
|
549
|
-
)
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
def _iso_or_none(moment: datetime | None) -> str | None:
|
|
553
|
-
return moment.isoformat() if moment else None
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
def reading_payload(reading: AccountReading) -> dict[str, object]:
|
|
557
|
-
"""Shape one account's reading as the JSON object a report prints.
|
|
558
|
-
|
|
559
|
-
Args:
|
|
560
|
-
reading: One account's reading.
|
|
561
|
-
|
|
562
|
-
Returns:
|
|
563
|
-
The name, percent left, each window, and any unread reason.
|
|
564
|
-
"""
|
|
565
|
-
if reading.meters is None:
|
|
566
|
-
return {
|
|
567
|
-
"name": reading.name,
|
|
568
|
-
"percent_left": None,
|
|
569
|
-
"unread": reading.unread_reason,
|
|
570
|
-
}
|
|
571
|
-
return {
|
|
572
|
-
"name": reading.name,
|
|
573
|
-
"percent_left": reading.meters.percent_left,
|
|
574
|
-
"windows": [
|
|
575
|
-
{
|
|
576
|
-
"minutes": each_window.duration_minutes,
|
|
577
|
-
"used_percent": each_window.used_percent,
|
|
578
|
-
"resets_at": _iso_or_none(each_window.resets_at),
|
|
579
|
-
}
|
|
580
|
-
for each_window in reading.meters.all_windows
|
|
581
|
-
],
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
def decision_payload(
|
|
586
|
-
decision: CodexAccountDecision, all_readings: Sequence[AccountReading]
|
|
587
|
-
) -> dict[str, object]:
|
|
588
|
-
"""Shape a decision as the JSON object the runner job reads.
|
|
589
|
-
|
|
590
|
-
Args:
|
|
591
|
-
decision: The chosen tier and account.
|
|
592
|
-
all_readings: Every account's reading, for the report.
|
|
593
|
-
|
|
594
|
-
Returns:
|
|
595
|
-
The tier, account, Codex home, room, stop point, reason, and all readings.
|
|
596
|
-
"""
|
|
597
|
-
is_runnable = decision.tier != TIER_WAIT and decision.reading is not None
|
|
598
|
-
chosen = decision.reading if is_runnable else None
|
|
599
|
-
return {
|
|
600
|
-
"tier": decision.tier,
|
|
601
|
-
"account": chosen.name if chosen else None,
|
|
602
|
-
"codex_home": str(chosen.codex_home) if chosen else None,
|
|
603
|
-
"percent_left": chosen.meters.percent_left
|
|
604
|
-
if chosen and chosen.meters
|
|
605
|
-
else None,
|
|
606
|
-
"stop_below_percent": decision.stop_below_percent,
|
|
607
|
-
"reason": decision.reason,
|
|
608
|
-
"accounts": [reading_payload(each_reading) for each_reading in all_readings],
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
def _meter_reader(codex_path_argument: Path | None) -> MeterReader:
|
|
613
|
-
codex_path = resolve_codex_path(codex_path_argument)
|
|
614
|
-
return lambda codex_home: read_codex_meters(codex_path, codex_home)
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
def _run_choose(arguments: argparse.Namespace) -> int:
|
|
618
|
-
read_meters = _meter_reader(arguments.codex_path)
|
|
619
|
-
all_readings = [
|
|
620
|
-
read_account(each_name, arguments.profiles_root, read_meters)
|
|
621
|
-
for each_name in codex_account_names(arguments.profiles_root)
|
|
622
|
-
]
|
|
623
|
-
print(
|
|
624
|
-
json.dumps(decision_payload(choose_codex_account(all_readings), all_readings))
|
|
625
|
-
)
|
|
626
|
-
return 0
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
def _require_known_account(name: str, profiles_root: Path) -> None:
|
|
630
|
-
all_known = codex_account_names(profiles_root)
|
|
631
|
-
if name not in all_known:
|
|
632
|
-
raise CodexAccountNameError(
|
|
633
|
-
UNKNOWN_ACCOUNT_TEMPLATE.format(
|
|
634
|
-
name=name, known=SETUP_NAMES_SEPARATOR.join(all_known)
|
|
635
|
-
)
|
|
636
|
-
)
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
def _run_check(arguments: argparse.Namespace) -> int:
|
|
640
|
-
_require_known_account(arguments.account, arguments.profiles_root)
|
|
641
|
-
reading = read_account(
|
|
642
|
-
arguments.account, arguments.profiles_root, _meter_reader(arguments.codex_path)
|
|
643
|
-
)
|
|
644
|
-
print(json.dumps(reading_payload(reading)))
|
|
645
|
-
if reading.meters is not None and reading.meters.percent_left > arguments.floor:
|
|
646
|
-
return EXIT_CODE_ROOM
|
|
647
|
-
return EXIT_CODE_NO_ROOM
|
|
648
|
-
|
|
649
363
|
|
|
650
364
|
def _run_sync(arguments: argparse.Namespace) -> int:
|
|
651
365
|
now = datetime.now(timezone.utc)
|
|
@@ -715,18 +429,10 @@ def _add_launcher_commands(
|
|
|
715
429
|
|
|
716
430
|
def _build_argument_parser() -> argparse.ArgumentParser:
|
|
717
431
|
parser = argparse.ArgumentParser(
|
|
718
|
-
description="
|
|
432
|
+
description="Keep Codex account homes and launchers ready."
|
|
719
433
|
)
|
|
720
434
|
parser.add_argument("--profiles-root", type=Path, default=default_profiles_root())
|
|
721
|
-
parser.add_argument("--codex-path", type=Path, default=None)
|
|
722
435
|
all_commands = parser.add_subparsers(dest="command", required=True)
|
|
723
|
-
all_commands.add_parser("choose").set_defaults(run=_run_choose)
|
|
724
|
-
check_parser = all_commands.add_parser("check")
|
|
725
|
-
check_parser.add_argument("account")
|
|
726
|
-
check_parser.add_argument(
|
|
727
|
-
"--floor", type=float, default=LUNA_TIER_STOP_PERCENT_LEFT
|
|
728
|
-
)
|
|
729
|
-
check_parser.set_defaults(run=_run_check)
|
|
730
436
|
sync_parser = all_commands.add_parser("sync")
|
|
731
437
|
_add_main_home_argument(sync_parser)
|
|
732
438
|
sync_parser.set_defaults(run=_run_sync)
|
|
@@ -741,9 +447,8 @@ def main(all_command_arguments: list[str]) -> int:
|
|
|
741
447
|
all_command_arguments: Command-line arguments after the program name.
|
|
742
448
|
|
|
743
449
|
Returns:
|
|
744
|
-
Zero for
|
|
745
|
-
|
|
746
|
-
parser with two.
|
|
450
|
+
Zero for sync, setup, and install. An invalid account name exits
|
|
451
|
+
through the parser with two.
|
|
747
452
|
"""
|
|
748
453
|
parser = _build_argument_parser()
|
|
749
454
|
arguments = parser.parse_args(all_command_arguments)
|