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.
@@ -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.5",
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
 
@@ -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 os.name == "nt":
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 == "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"])
@@ -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.os, "name", "posix")
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.os, "name", "nt")
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