claude-dev-env 8.12.0 → 8.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/pull-request/scripts/test_pull_request.py +1 -1
- package/.agents/skills-archived/pr-converge/scripts/test_check_convergence.py +4 -4
- package/_shared/advisor/reference/astra-rung.md +7 -7
- package/_shared/advisor/reference/third-party-bind.md +2 -2
- package/_shared/advisor/scripts/codex_astra_advisor.py +33 -26
- package/_shared/advisor/scripts/codex_astra_preflight.py +57 -55
- package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +8 -8
- package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +69 -23
- package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +4 -4
- package/_shared/pr-loop/scripts/check_convergence.py +54 -49
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/run_constants.py +4 -0
- package/_shared/pr-loop/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +10 -1
- package/_shared/pr-loop/scripts/run_codex_review.py +4 -6
- package/_shared/pr-loop/scripts/test_check_convergence.py +6 -6
- package/_shared/pr-loop/scripts/test_check_convergence_codex.py +73 -31
- package/_shared/pr-loop/scripts/test_check_convergence_contract.py +2 -2
- package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +2 -2
- package/bin/git_hooks_installer.test.mjs +1 -1
- package/bin/install-constants.mjs +1 -6
- package/bin/install.cursor-rules.test.mjs +24 -51
- package/bin/install.mjs +16 -70
- package/docs/codex-accounts.md +79 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +5 -5
- package/hooks/blocking/test_pii_scanner.py +1 -1
- package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +2 -2
- package/hooks/git-hooks/conftest.py +16 -0
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +1 -1
- package/hooks/git-hooks/test_post_commit.py +1 -1
- package/hooks/git-hooks/test_pre_commit.py +1 -1
- package/hooks/git-hooks/test_pre_push.py +2 -2
- package/hooks/git-hooks/test_verification_start.py +1 -1
- package/hooks/git-hooks/verification_notice.py +2 -2
- package/hooks/git-hooks/verification_notice_context.py +21 -2
- package/hooks/git-hooks/verification_start.py +2 -3
- package/package.json +1 -1
- package/rules/durable-post-artifacts.md +4 -3
- package/scripts/claude_account_profile.py +21 -9
- package/scripts/codex_account_choice.py +396 -0
- package/scripts/codex_account_meters.py +372 -0
- package/scripts/dev_env_scripts_constants/codex_account_constants.py +141 -0
- package/scripts/dev_env_scripts_constants/private_term_constants.py +17 -2
- package/scripts/private_term_scan.py +6 -2
- package/scripts/repository_checks/config/constants.py +1 -0
- package/scripts/repository_checks/tracked_private_terms.py +7 -0
- package/scripts/test_claude_account_profile.py +12 -0
- package/scripts/test_codex_account_choice.py +276 -0
- package/scripts/test_codex_account_meters.py +205 -0
- package/scripts/tests/test_automatic_advisory.py +3 -3
- package/scripts/tests/test_automatic_advisory_polling.py +1 -1
- package/scripts/tests/test_automatic_advisory_recovery.py +1 -1
- package/scripts/tests/test_closed_pr_label.py +1 -1
- package/scripts/tests/test_private_term_scan.py +28 -0
- package/scripts/tests/test_review_closure.py +2 -2
- package/scripts/tests/test_review_closure_model.py +2 -2
- package/scripts/tests/test_rules.py +2 -83
- package/scripts/tests/test_tracked_private_terms.py +15 -0
- package/scripts/tests/test_verification_notice.py +29 -13
- package/.agents/skills/cert-classification-rule/SKILL.md +0 -224
- package/.agents/skills/cert-classification-rule/scripts/resort_cert_folders.py +0 -158
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -82
- package/_shared/pr-loop/scripts/codex_usage_probe.py +0 -573
- package/_shared/pr-loop/scripts/test_codex_usage_probe.py +0 -683
- package/scripts/sync_to_cursor/AGENTS.md +0 -1
- package/scripts/sync_to_cursor/CLAUDE.md +0 -1
- package/scripts/sync_to_cursor/__init__.py +0 -13
- package/scripts/sync_to_cursor/canonical_docs.py +0 -66
- package/scripts/sync_to_cursor/config/__init__.py +0 -8
- package/scripts/sync_to_cursor/engine.py +0 -219
- package/scripts/sync_to_cursor/hashing.py +0 -7
- package/scripts/sync_to_cursor/paths.py +0 -18
- package/scripts/sync_to_cursor/rules.py +0 -494
- package/scripts/sync_to_cursor.py +0 -22
- package/scripts/tests/test_engine.py +0 -101
- package/scripts/tests/test_sync_to_cursor.py +0 -315
|
@@ -22,7 +22,7 @@ import pull_request
|
|
|
22
22
|
REPOSITORY = "jl-cmd/claude-dev-env"
|
|
23
23
|
PR_NUMBER = "2518"
|
|
24
24
|
PR_TITLE = "feat(skills): add guarded pull request publication"
|
|
25
|
-
ACCOUNT_A = "
|
|
25
|
+
ACCOUNT_A = "octocat"
|
|
26
26
|
ACCOUNT_B = "ReviewBot"
|
|
27
27
|
BODY_FILENAME = "pull-request-body.md"
|
|
28
28
|
LINTER_PATH = _SCRIPTS_DIRECTORY.parents[3] / "scripts/durable_post_lint.py"
|
|
@@ -135,7 +135,7 @@ def should_pass_when_clean_bugteam_review_present_on_current_head(
|
|
|
135
135
|
check_convergence, "_gh_api_paginated", _make_stub_gh_paginated(reviews_payload)
|
|
136
136
|
)
|
|
137
137
|
passed, detail = check_convergence._check_bugteam_clean(
|
|
138
|
-
owner="
|
|
138
|
+
owner="octocat", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
|
|
139
139
|
)
|
|
140
140
|
assert passed is True
|
|
141
141
|
assert "clean bugteam audit" in detail
|
|
@@ -157,7 +157,7 @@ def should_fail_when_dirty_bugteam_review_present_on_current_head(
|
|
|
157
157
|
check_convergence, "_gh_api_paginated", _make_stub_gh_paginated(reviews_payload)
|
|
158
158
|
)
|
|
159
159
|
passed, detail = check_convergence._check_bugteam_clean(
|
|
160
|
-
owner="
|
|
160
|
+
owner="octocat", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
|
|
161
161
|
)
|
|
162
162
|
assert passed is False
|
|
163
163
|
assert "dirty bugteam audit" in detail
|
|
@@ -185,7 +185,7 @@ def should_fail_when_no_bugteam_review_present_on_current_head(
|
|
|
185
185
|
check_convergence, "_gh_api_paginated", _make_stub_gh_paginated(reviews_payload)
|
|
186
186
|
)
|
|
187
187
|
passed, detail = check_convergence._check_bugteam_clean(
|
|
188
|
-
owner="
|
|
188
|
+
owner="octocat", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
|
|
189
189
|
)
|
|
190
190
|
assert passed is False
|
|
191
191
|
assert "no bugteam review found" in detail
|
|
@@ -204,7 +204,7 @@ def should_fail_with_shape_detail_when_gh_returns_non_list_payload(
|
|
|
204
204
|
check_convergence, "_gh_api_paginated", stub_gh_api_paginated_returning_object
|
|
205
205
|
)
|
|
206
206
|
passed, detail = check_convergence._check_bugteam_clean(
|
|
207
|
-
owner="
|
|
207
|
+
owner="octocat", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
|
|
208
208
|
)
|
|
209
209
|
assert passed is False
|
|
210
210
|
assert "unexpected gh api response shape" in detail
|
|
@@ -14,26 +14,26 @@ A Windows `setx` write only updates the persisted user environment; only a proce
|
|
|
14
14
|
|
|
15
15
|
`ADVISOR_EFFORT` selects Codex `model_reasoning_effort` for Astra: `low`, `medium`, `high`, `xhigh`, or `max`. The policy default is `medium`, which sends `model_reasoning_effort="medium"`. Astra Xhigh and Max requests route to Medium. Pass `--effort <level>` on the helper to set effort for that Astra run without changing the environment. An unset value uses the policy default. An unknown value blocks advisor routing.
|
|
16
16
|
|
|
17
|
-
Every fallback reply carries a `fallback_kind` field. `declined` means policy closed the rung (flag off,
|
|
17
|
+
Every fallback reply carries a `fallback_kind` field. `declined` means policy closed the rung (flag off, or the account picker named no Codex account on the `normal` tier). `broken` means the Astra path itself failed (missing executable, spawn error, timeout, malformed reply). A `broken` fallback is a defect to report.
|
|
18
18
|
|
|
19
19
|
## Preflight
|
|
20
20
|
|
|
21
|
-
Flag on:
|
|
21
|
+
Flag on: the helper asks the Codex account picker for an account first.
|
|
22
22
|
|
|
23
23
|
```
|
|
24
|
-
python ~/.claude/
|
|
24
|
+
python ~/.claude/scripts/codex_account_choice.py choose
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
**GOTCHA.
|
|
27
|
+
**GOTCHA. Picker path.** `codex_astra_advisor.resolve_account_picker_path` builds the picker path from the helper's own location: two directories above `_shared/advisor/scripts`, then `scripts/codex_account_choice.py`. Installed, that is `~/.claude/scripts/codex_account_choice.py`. Do not hunt other copies. Repo home: `packages/claude-dev-env/scripts/`. The accounts, their order, and sign-in steps are in [`codex-accounts.md`](../../../docs/codex-accounts.md).
|
|
28
28
|
|
|
29
|
-
The shared entry point is `~/.claude/_shared/advisor/scripts/codex_astra_advisor.py`. It
|
|
29
|
+
The shared entry point is `~/.claude/_shared/advisor/scripts/codex_astra_advisor.py`. It runs the picker and owns Astra bind or resume parsing. Bind with `python ~/.claude/_shared/advisor/scripts/codex_astra_advisor.py --bind --enable-astra --cwd <repo-root>` and pipe the charter on stdin. Resume with `--resume <session_id>` and pipe the delta consult on stdin.
|
|
30
30
|
|
|
31
|
-
The gate passes only when the
|
|
31
|
+
The gate passes only when the picker exits 0 and answers `tier: normal` with a `codex_home` and a finite `percent_left`. The helper then runs Codex with `CODEX_HOME` set to that `codex_home`. A `luna` or `wait` answer fails closed when Fable did not bind.
|
|
32
32
|
|
|
33
33
|
## Branches
|
|
34
34
|
|
|
35
35
|
**Preflight pass.** Bind one Codex CLI session at `gpt-6-astra` with `model_reasoning_effort` set from the policy-selected `ADVISOR_EFFORT` value, `--sandbox read-only`, and JSON output. The helper receives the standing-reviewer charter on stdin and returns only parsed ENDORSE / CORRECTION / PLAN / STOP guidance with a session ID.
|
|
36
36
|
|
|
37
|
-
**Preflight fail.**
|
|
37
|
+
**Preflight fail.** A picker failure, non-zero exit, timeout, malformed answer, or a `luna` or `wait` tier fails closed when Fable did not bind.
|
|
38
38
|
|
|
39
39
|
The helper owns the Astra attempt and returns an explicit fallback result. The consuming advisor path owns the Fable bind. Apply the same gate to every Astra attempt, including resume.
|
|
@@ -5,13 +5,13 @@ Open this when binding or re-binding the advisor from a third-party (non-Claude,
|
|
|
5
5
|
|
|
6
6
|
## GOTCHA: Cursor Astra first shot
|
|
7
7
|
|
|
8
|
-
Cursor is a third-party host. When the walk reaches Astra (`ADVISOR_ASTRA=1` / `--enable-astra` after Fable is out of usage, or the user asks for Astra), bind Astra on the **first** tool call through the headless Codex helper. Do not use the Agent or Task tool, do not spawn Grok as a substitute, and do not search the filesystem for
|
|
8
|
+
Cursor is a third-party host. When the walk reaches Astra (`ADVISOR_ASTRA=1` / `--enable-astra` after Fable is out of usage, or the user asks for Astra), bind Astra on the **first** tool call through the headless Codex helper. Do not use the Agent or Task tool, do not spawn Grok as a substitute, and do not search the filesystem for the Codex account picker.
|
|
9
9
|
|
|
10
10
|
```
|
|
11
11
|
python "$HOME/.claude/_shared/advisor/scripts/codex_astra_advisor.py" --bind --enable-astra --cwd <repo-root>
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Pipe the standing-reviewer charter on stdin. Persist `session_id` from the JSON reply. Later consults: `--resume <session_id>` with the delta on stdin. Capture helper / `codex` stdout as UTF-8 on Windows (`encoding="utf-8"`, `errors="replace"`).
|
|
14
|
+
Pipe the standing-reviewer charter on stdin. Persist `session_id` from the JSON reply. Later consults: `--resume <session_id>` with the delta on stdin. Capture helper / `codex` stdout as UTF-8 on Windows (`encoding="utf-8"`, `errors="replace"`). Picker path and preflight: [`astra-rung.md`](astra-rung.md).
|
|
15
15
|
|
|
16
16
|
When the Astra flag is off, follow the Claude-chain steps below.
|
|
17
17
|
|
|
@@ -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
|
-
|
|
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
|
|
180
|
-
"""Return the Codex
|
|
177
|
+
def resolve_account_picker_path() -> Path:
|
|
178
|
+
"""Return the Codex account picker in the scripts directory beside this shared tree.
|
|
181
179
|
|
|
182
|
-
|
|
183
|
-
|
|
180
|
+
::
|
|
181
|
+
|
|
182
|
+
~/.claude/_shared/advisor/scripts -> ~/.claude/scripts/codex_account_choice.py
|
|
184
183
|
|
|
185
184
|
Returns:
|
|
186
|
-
Path to
|
|
185
|
+
Path to ``codex_account_choice.py``.
|
|
187
186
|
"""
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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:
|
|
338
|
-
|
|
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
|
|
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,
|
|
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
|
-
"""
|
|
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
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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(
|
|
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=
|
|
75
|
+
timeout=ASTRA_ACCOUNT_PICK_TIMEOUT_SECONDS,
|
|
71
76
|
)
|
|
72
77
|
|
|
73
78
|
|
|
74
|
-
def
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
92
|
+
picker_path: Path,
|
|
92
93
|
process_runner: Callable[..., subprocess.CompletedProcess[str]],
|
|
93
94
|
) -> AstraPreflight:
|
|
94
|
-
"""Run the
|
|
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
|
-
|
|
98
|
-
process_runner: Callable that runs the
|
|
104
|
+
picker_path: Path to ``codex_account_choice.py``.
|
|
105
|
+
process_runner: Callable that runs the picker.
|
|
99
106
|
|
|
100
107
|
Returns:
|
|
101
|
-
Eligibility,
|
|
108
|
+
Eligibility, the chosen account's percent left and Codex home, reason, and fallback kind.
|
|
102
109
|
"""
|
|
103
110
|
try:
|
|
104
|
-
|
|
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"{
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
|
51
|
+
def _pick(payload: object, returncode: int = 0) -> subprocess.CompletedProcess[str]:
|
|
46
52
|
return subprocess.CompletedProcess(
|
|
47
|
-
[sys.executable, str(
|
|
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]],
|
|
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
|
|
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
|
|
159
|
-
path = astra_advisor.
|
|
160
|
-
assert path ==
|
|
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
|
-
|
|
184
|
+
|
|
185
|
+
def test_preflight_accepts_the_normal_tier_and_keeps_its_codex_home() -> None:
|
|
164
186
|
preflight = astra_advisor.run_astra_preflight(
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
180
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
16
|
-
|
|
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(
|
|
21
|
+
preflight = run_astra_preflight(picker_path, process_runner)
|
|
22
22
|
|
|
23
23
|
assert not preflight.eligible
|
|
24
24
|
assert preflight.fallback_kind == "broken"
|