claude-dev-env 8.48.6 → 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.
@@ -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, or its command could not start while no Codex roster is configured. |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.48.6",
3
+ "version": "8.48.7",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
 
@@ -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 == "exhausted"
711
- assert outcome.returncode == WAIT_EXIT_CODE
754
+ assert outcome.status == "start_failed"
755
+ assert outcome.returncode == 127
712
756
 
713
757
 
714
758
  @pytest.mark.parametrize("marked_name", ["retired", "first"])