claude-dev-env 8.48.5 → 8.48.7
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/docs/account-broker.md +1 -1
- package/hooks/routing/test_subagent_model_pin_hook.py +1 -0
- package/package.json +1 -1
- package/scripts/account_broker.py +3 -0
- package/scripts/codex_compat_materializer.py +6 -1
- package/scripts/test_account_broker.py +46 -2
- package/scripts/tests/test_codex_compat_materializer.py +2 -2
package/docs/account-broker.md
CHANGED
|
@@ -27,7 +27,7 @@ The broker stores cached meters, spent marks, and Claude session bindings in one
|
|
|
27
27
|
| `0` | `choose` or `check` found an account, or the job exited successfully. |
|
|
28
28
|
| `3` | No account clears its floor. The decision includes `resets_at`. |
|
|
29
29
|
| `4` | A Claude job stopped with `advisor_blocked`. |
|
|
30
|
-
| `127` | A Codex job timed out,
|
|
30
|
+
| `127` | A Codex job timed out, its command could not start while no Codex roster is configured, or the command could not start on any account in the roster. |
|
|
31
31
|
| Other command code | `run` returns the command's exit code after a served response. |
|
|
32
32
|
|
|
33
33
|
`run` writes a JSON report with `product`, `command`, `events`, and `final_decision`. A wait report has `final_decision.action` equal to `wait` and a UTC `final_decision.resets_at`.
|
|
@@ -31,6 +31,7 @@ def test_should_move_the_spawn_to_opus(requested_model: object) -> None:
|
|
|
31
31
|
if requested_model is not None:
|
|
32
32
|
tool_input["model"] = requested_model
|
|
33
33
|
decision = json.loads(_run_hook(tool_input))["hookSpecificOutput"]
|
|
34
|
+
assert decision["hookEventName"] == "PreToolUse"
|
|
34
35
|
assert "permissionDecision" not in decision
|
|
35
36
|
assert decision["updatedInput"] == {**SPAWN_INPUT, "model": "opus"}
|
|
36
37
|
|
package/package.json
CHANGED
|
@@ -234,6 +234,9 @@ def _prepare_run(
|
|
|
234
234
|
|
|
235
235
|
def _wait_outcome(context: _RunContext, decision: Decision) -> JobOutcome:
|
|
236
236
|
context.report.final_decision = decision
|
|
237
|
+
all_attempt_statuses = {each_status for _, each_status in context.all_attempts}
|
|
238
|
+
if all_attempt_statuses == {"start_failed"}:
|
|
239
|
+
return JobOutcome(COMMAND_MISSING_EXIT_CODE, "", "", None, tuple(context.all_attempts), "start_failed", None, None)
|
|
237
240
|
status = "exhausted" if context.all_attempts else "wait"
|
|
238
241
|
return JobOutcome(WAIT_EXIT_CODE, "", "", None, tuple(context.all_attempts), status, None, decision.resets_at)
|
|
239
242
|
|
|
@@ -684,6 +684,11 @@ def _is_code_rules_enforcer_hook(all_hook_record: dict[str, object]) -> bool:
|
|
|
684
684
|
)
|
|
685
685
|
|
|
686
686
|
|
|
687
|
+
def _is_windows_host() -> bool:
|
|
688
|
+
"""Report whether hook command paths compare without regard to case."""
|
|
689
|
+
return os.name == "nt"
|
|
690
|
+
|
|
691
|
+
|
|
687
692
|
def _is_managed_codex_enforcer_hook(
|
|
688
693
|
all_hook_record: dict[str, object], managed_command: str
|
|
689
694
|
) -> bool:
|
|
@@ -693,7 +698,7 @@ def _is_managed_codex_enforcer_hook(
|
|
|
693
698
|
return False
|
|
694
699
|
normalized_command = command.replace("\\", path_separator)
|
|
695
700
|
normalized_managed_command = managed_command.replace("\\", path_separator)
|
|
696
|
-
if
|
|
701
|
+
if _is_windows_host():
|
|
697
702
|
return normalized_command.casefold() == normalized_managed_command.casefold()
|
|
698
703
|
return normalized_command == normalized_managed_command
|
|
699
704
|
|
|
@@ -418,6 +418,50 @@ def test_should_leave_later_choices_open_after_start_failure(
|
|
|
418
418
|
assert json.loads(capsys.readouterr().out)["decision"]["account"] == "first"
|
|
419
419
|
|
|
420
420
|
|
|
421
|
+
@pytest.mark.parametrize("product", (Product.CLAUDE, Product.CODEX))
|
|
422
|
+
def test_should_exit_127_when_every_account_fails_to_start(
|
|
423
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, product: Product
|
|
424
|
+
) -> None:
|
|
425
|
+
accounts = (_account("first", product), _account("second", product))
|
|
426
|
+
monkeypatch.setitem(account_broker.all_product_adapters, product, _adapter(accounts, {
|
|
427
|
+
"first": _meters(80, 80), "second": _meters(70, 70)
|
|
428
|
+
}))
|
|
429
|
+
monkeypatch.setattr(account_broker.sys, "stdin", io.TextIOWrapper(io.BytesIO(b""), encoding="utf-8"))
|
|
430
|
+
|
|
431
|
+
def runner(argv: object, **options: object) -> subprocess.CompletedProcess[str]:
|
|
432
|
+
raise OSError("missing command")
|
|
433
|
+
|
|
434
|
+
with account_broker.override_subprocess_runner(runner):
|
|
435
|
+
outcome, _ = account_broker._execute(product, ("job",), now=datetime.now(timezone.utc))
|
|
436
|
+
code = account_broker.main(("run", "--product", product.value, "--report", str(tmp_path / "report.json"), "--", "job"))
|
|
437
|
+
|
|
438
|
+
assert outcome.status == "start_failed"
|
|
439
|
+
assert outcome.returncode == 127
|
|
440
|
+
assert outcome.wait_reset_at is None
|
|
441
|
+
assert code == 127
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def test_should_report_exhausted_when_start_failure_meets_usage_limit(
|
|
445
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
446
|
+
) -> None:
|
|
447
|
+
accounts = (_account("first"), _account("second"))
|
|
448
|
+
monkeypatch.setitem(account_broker.all_product_adapters, Product.CODEX, _adapter(accounts, {
|
|
449
|
+
"first": _meters(80, 80), "second": _meters(70, 70)
|
|
450
|
+
}))
|
|
451
|
+
|
|
452
|
+
def runner(argv: object, **options: object) -> subprocess.CompletedProcess[str]:
|
|
453
|
+
if Path(options["env"]["CODEX_HOME"]).name == "first":
|
|
454
|
+
raise OSError("missing command")
|
|
455
|
+
return subprocess.CompletedProcess(argv, 1, "", "rate limit")
|
|
456
|
+
|
|
457
|
+
with account_broker.override_subprocess_runner(runner):
|
|
458
|
+
outcome, _ = account_broker._execute(Product.CODEX, ("job",), now=datetime.now(timezone.utc))
|
|
459
|
+
|
|
460
|
+
assert outcome.attempts == (("first", "start_failed"), ("second", "usage_limited"))
|
|
461
|
+
assert outcome.status == "exhausted"
|
|
462
|
+
assert outcome.returncode == 3
|
|
463
|
+
|
|
464
|
+
|
|
421
465
|
def test_should_stop_after_timeout_without_running_job_again(
|
|
422
466
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
423
467
|
) -> None:
|
|
@@ -707,8 +751,8 @@ def test_should_record_start_failure_without_launching_a_command_missing_from_pa
|
|
|
707
751
|
outcome, _ = account_broker._execute(Product.CLAUDE, ("claude", "-p"), now=NOW)
|
|
708
752
|
|
|
709
753
|
assert outcome.attempts == (("extra", "start_failed"),)
|
|
710
|
-
assert outcome.status == "
|
|
711
|
-
assert outcome.returncode ==
|
|
754
|
+
assert outcome.status == "start_failed"
|
|
755
|
+
assert outcome.returncode == 127
|
|
712
756
|
|
|
713
757
|
|
|
714
758
|
@pytest.mark.parametrize("marked_name", ["retired", "first"])
|
|
@@ -1290,7 +1290,7 @@ def test_posix_case_distinct_enforcer_path_remains_foreign(
|
|
|
1290
1290
|
target_manifest = _codex_hook_manifest(
|
|
1291
1291
|
_codex_hook_group(managed_hook, foreign_hook)
|
|
1292
1292
|
)
|
|
1293
|
-
monkeypatch.setattr(materializer
|
|
1293
|
+
monkeypatch.setattr(materializer, "_is_windows_host", lambda: False)
|
|
1294
1294
|
|
|
1295
1295
|
projected_manifest, _, _ = _publish_codex_hook_fixture(
|
|
1296
1296
|
source, target, target_manifest
|
|
@@ -1315,7 +1315,7 @@ def test_windows_case_variant_enforcer_path_is_managed(
|
|
|
1315
1315
|
target_manifest = _codex_hook_manifest(
|
|
1316
1316
|
_codex_hook_group({**managed_hook, "command": case_variant_command})
|
|
1317
1317
|
)
|
|
1318
|
-
monkeypatch.setattr(materializer
|
|
1318
|
+
monkeypatch.setattr(materializer, "_is_windows_host", lambda: True)
|
|
1319
1319
|
|
|
1320
1320
|
projected_manifest, _, _ = _publish_codex_hook_fixture(
|
|
1321
1321
|
source, target, target_manifest
|