claude-dev-env 8.46.0 → 8.46.2

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,6 +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
31
  | Other command code | `run` returns the command's exit code after a served response. |
31
32
 
32
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`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.46.0",
3
+ "version": "8.46.2",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -263,7 +263,8 @@ def _attempt_once(context: _RunContext, decision: Decision) -> JobOutcome | None
263
263
  _record_spent_attempt(context, account, status, COMMAND_MISSING_EXIT_CODE)
264
264
  if isinstance(error, subprocess.TimeoutExpired) or not context.all_readings:
265
265
  context.report.final_decision = decision
266
- return JobOutcome(COMMAND_MISSING_EXIT_CODE, "", str(error), account.name, tuple(context.all_attempts), "advisor_blocked", None, None)
266
+ final_status = "advisor_blocked" if context.product is Product.CLAUDE else status
267
+ return JobOutcome(COMMAND_MISSING_EXIT_CODE, "", str(error), account.name, tuple(context.all_attempts), final_status, None, None)
267
268
  return None
268
269
  combined = f"{completion.stdout}{completion.stderr}".casefold()
269
270
  is_limited = completion.returncode != 0 and any(
@@ -435,13 +435,44 @@ def test_should_stop_after_timeout_without_running_job_again(
435
435
 
436
436
  assert invoked_homes == ["first"]
437
437
  assert outcome.attempts == (("first", "timeout"),)
438
- assert outcome.status == "advisor_blocked"
438
+ assert outcome.status == "timeout"
439
439
  assert outcome.account_name == "first"
440
440
  assert report.final_decision.account.name == "first"
441
441
  assert account_broker.main(("choose", "--product", "codex")) == 0
442
442
  assert json.loads(capsys.readouterr().out)["decision"]["account"] == "first"
443
443
 
444
444
 
445
+ @pytest.mark.parametrize(
446
+ ("product", "roster_names", "start_error", "expected_code"),
447
+ (
448
+ (Product.CLAUDE, ("first",), subprocess.TimeoutExpired("job", 1), 4),
449
+ (Product.CODEX, ("first",), subprocess.TimeoutExpired("job", 1), 127),
450
+ (Product.CODEX, (), OSError("missing command"), 127),
451
+ ),
452
+ )
453
+ def test_should_exit_four_only_for_a_blocked_claude_job(
454
+ monkeypatch: pytest.MonkeyPatch,
455
+ tmp_path: Path,
456
+ product: Product,
457
+ roster_names: tuple[str, ...],
458
+ start_error: Exception,
459
+ expected_code: int,
460
+ ) -> None:
461
+ accounts = tuple(_account(each_name, product) for each_name in roster_names)
462
+ monkeypatch.setitem(account_broker.all_product_adapters, product, _adapter(accounts, {
463
+ each_name: _meters(80, 80) for each_name in roster_names
464
+ }))
465
+ monkeypatch.setattr(account_broker.sys, "stdin", io.TextIOWrapper(io.BytesIO(b""), encoding="utf-8"))
466
+
467
+ def runner(argv: object, **options: object) -> subprocess.CompletedProcess[str]:
468
+ raise start_error
469
+
470
+ with account_broker.override_subprocess_runner(runner):
471
+ code = account_broker.main(("run", "--product", product.value, "--report", str(tmp_path / "report.json"), "--", "job"))
472
+
473
+ assert code == expected_code
474
+
475
+
445
476
  def test_should_keep_usage_limited_account_spent_for_later_choices(
446
477
  monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
447
478
  ) -> None:
@@ -3,11 +3,12 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import ast
6
- import os
7
6
  import re
8
7
  from collections import Counter
9
8
  from pathlib import Path
10
9
 
10
+ import pytest
11
+
11
12
 
12
13
  _PACKAGE_ROOT = Path(__file__).resolve().parents[2]
13
14
  _REPOSITORY_ROOT = _PACKAGE_ROOT.parent.parent
@@ -118,28 +119,31 @@ def _collect_citation_texts() -> list[tuple[Path, str]]:
118
119
 
119
120
  def test_should_resolve_code_rules_anchors_and_sections() -> None:
120
121
  index_text = _INDEX_PATH.read_text(encoding="utf-8")
121
- all_citations = _collect_citation_texts()
122
- injected_anchor = os.environ.get("CODE_RULES_INDEX_TEST_ANCHOR")
123
- if injected_anchor:
124
- all_citations.append((_INDEX_PATH, "CODE_RULES.md" + "#" + injected_anchor))
125
- _assert_citations_resolve(index_text, all_citations)
122
+ _assert_citations_resolve(index_text, _collect_citation_texts())
123
+
124
+
125
+ def test_should_report_dead_code_rules_anchor() -> None:
126
+ index_text = _INDEX_PATH.read_text(encoding="utf-8")
127
+ dead_citation = (_INDEX_PATH, "CODE_RULES.md" + "#no-such-heading")
128
+ with pytest.raises(AssertionError, match="dead CODE_RULES anchor #no-such-heading"):
129
+ _assert_citations_resolve(index_text, [dead_citation])
126
130
 
127
131
 
128
- def _assert_destination_resolves(family_path: Path, destination: str) -> None:
132
+ def _assert_destination_resolves(markdown_path: Path, destination: str) -> None:
129
133
  if "://" in destination or destination.startswith("#"):
130
134
  return
131
135
  path_text, _, anchor_text = destination.partition("#")
132
- destination_path = family_path.parent / path_text
133
- assert destination_path.is_file(), f"{family_path}: {destination}"
136
+ destination_path = markdown_path.parent / path_text
137
+ assert destination_path.is_file(), f"{markdown_path}: {destination}"
134
138
  if anchor_text:
135
139
  destination_text = destination_path.read_text(encoding="utf-8")
136
140
  assert anchor_text in _heading_slugs(destination_text), (
137
- f"{family_path}: {destination}"
141
+ f"{markdown_path}: {destination}"
138
142
  )
139
143
 
140
144
 
141
- def test_should_resolve_every_family_relative_link() -> None:
142
- for each_family_path in _FAMILY_DIRECTORY.glob("*.md"):
143
- family_text = each_family_path.read_text(encoding="utf-8")
144
- for each_destination in _MARKDOWN_LINK.findall(family_text):
145
- _assert_destination_resolves(each_family_path, each_destination)
145
+ def test_should_resolve_every_index_and_family_relative_link() -> None:
146
+ for each_markdown_path in [_INDEX_PATH, *_FAMILY_DIRECTORY.glob("*.md")]:
147
+ markdown_text = each_markdown_path.read_text(encoding="utf-8")
148
+ for each_destination in _MARKDOWN_LINK.findall(markdown_text):
149
+ _assert_destination_resolves(each_markdown_path, each_destination)