claude-dev-env 8.43.1 → 8.43.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.43.1",
3
+ "version": "8.43.3",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -244,7 +244,7 @@ def _attempt_once(context: _RunContext, decision: Decision) -> JobOutcome | None
244
244
  except (OSError, subprocess.TimeoutExpired) as error:
245
245
  status = "timeout" if isinstance(error, subprocess.TimeoutExpired) else "start_failed"
246
246
  _record_spent_attempt(context, account, status, COMMAND_MISSING_EXIT_CODE)
247
- if not context.all_readings:
247
+ if isinstance(error, subprocess.TimeoutExpired) or not context.all_readings:
248
248
  context.report.final_decision = decision
249
249
  return JobOutcome(COMMAND_MISSING_EXIT_CODE, "", str(error), account.name, tuple(context.all_attempts), "advisor_blocked", None, None)
250
250
  return None
@@ -224,9 +224,13 @@ def _account_reset(reading: Reading, is_spent: bool = False) -> datetime | None:
224
224
  return None
225
225
  if is_spent:
226
226
  return min((reset for reset in (meters.session_resets_at, meters.weekly_resets_at) if reset is not None), default=None)
227
- if reading.account.product is Product.CLAUDE:
228
- if reading.account.is_main:
229
- return meters.weekly_resets_at
227
+ is_claude_main = reading.account.product is Product.CLAUDE and reading.account.is_main
228
+ if is_claude_main:
229
+ blocking = (
230
+ (meters.session_percent_left, FULL_PERCENT - ALL_CLAUDE_FLOORS["main_session_used_ceiling"], meters.session_resets_at),
231
+ (meters.weekly_percent_left, FULL_PERCENT - ALL_CLAUDE_FLOORS["main_weekly_used_ceiling"], meters.weekly_resets_at),
232
+ )
233
+ elif reading.account.product is Product.CLAUDE:
230
234
  blocking = (
231
235
  (meters.session_percent_left, FULL_PERCENT - ALL_CLAUDE_FLOORS["extra_session_used_ceiling"], meters.session_resets_at),
232
236
  (meters.weekly_percent_left, FULL_PERCENT - ALL_CLAUDE_FLOORS["extra_weekly_used_ceiling"], meters.weekly_resets_at),
@@ -236,7 +240,10 @@ def _account_reset(reading: Reading, is_spent: bool = False) -> datetime | None:
236
240
  (meters.session_percent_left, ALL_CODEX_FLOORS["luna_short_minimum_left"], meters.session_resets_at),
237
241
  (meters.weekly_percent_left, ALL_CODEX_FLOORS["luna_stop_left"], meters.weekly_resets_at),
238
242
  )
239
- return max((reset for room, floor, reset in blocking if room is not None and room <= floor and reset is not None), default=None)
243
+ all_blocking_resets = [reset for room, floor, reset in blocking if room is not None and room <= floor and reset is not None]
244
+ if is_claude_main and meters.weekly_resets_at is not None:
245
+ all_blocking_resets.append(meters.weekly_resets_at - ALL_CLAUDE_FLOORS["main_spend_window"])
246
+ return max(all_blocking_resets, default=None)
240
247
 
241
248
 
242
249
  def _rank_key(reading: Reading) -> float:
@@ -127,6 +127,28 @@ def test_should_prioritize_main_when_its_guard_passes() -> None:
127
127
  assert decision.account == main.account
128
128
 
129
129
 
130
+ @pytest.mark.parametrize(
131
+ ("short_left", "weekly_left", "weekly_reset", "expected_reset"),
132
+ (
133
+ (40, 20, NOW + timedelta(hours=4), NOW + timedelta(hours=2)),
134
+ (80, 5, NOW + timedelta(hours=4), NOW + timedelta(hours=4)),
135
+ (80, 50, NOW + timedelta(days=3), NOW + timedelta(days=2)),
136
+ ),
137
+ )
138
+ def test_should_wait_for_the_meter_or_window_that_blocks_main(
139
+ short_left: float, weekly_left: float, weekly_reset: datetime, expected_reset: datetime
140
+ ) -> None:
141
+ main = Reading(
142
+ _account("main", Product.CLAUDE, main=True),
143
+ _meters(short_left, weekly_left, short_reset=NOW + timedelta(hours=2), weekly_reset=weekly_reset),
144
+ )
145
+
146
+ decision = choose_from_readings(Product.CLAUDE, (main,), now=NOW)
147
+
148
+ assert decision.action == "wait"
149
+ assert decision.resets_at == expected_reset
150
+
151
+
130
152
  def test_should_try_next_account_after_usage_limit(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
131
153
  accounts = (_account("first"), _account("second"))
132
154
  adapter = _adapter(accounts, {"first": _meters(80, 80), "second": _meters(70, 70)})
@@ -361,12 +383,8 @@ def test_should_keep_spent_mark_until_reset(
361
383
  assert json.loads(capsys.readouterr().out)["decision"]["account"] == "second"
362
384
 
363
385
 
364
- @pytest.mark.parametrize(
365
- ("start_error", "expected_status"),
366
- ((OSError("missing command"), "start_failed"), (subprocess.TimeoutExpired("job", 1), "timeout")),
367
- )
368
386
  def test_should_leave_later_choices_open_after_start_failure(
369
- monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], start_error: Exception, expected_status: str
387
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
370
388
  ) -> None:
371
389
  accounts = (_account("first"), _account("second"))
372
390
  monkeypatch.setitem(account_broker.all_product_adapters, Product.CODEX, _adapter(accounts, {
@@ -374,12 +392,37 @@ def test_should_leave_later_choices_open_after_start_failure(
374
392
  }))
375
393
 
376
394
  def runner(argv: object, **options: object) -> subprocess.CompletedProcess[str]:
377
- raise start_error
395
+ raise OSError("missing command")
378
396
 
379
397
  with account_broker.override_subprocess_runner(runner):
380
398
  outcome, _ = account_broker._execute(Product.CODEX, ("job",), now=datetime.now(timezone.utc))
381
399
 
382
- assert outcome.attempts == (("first", expected_status), ("second", expected_status))
400
+ assert outcome.attempts == (("first", "start_failed"), ("second", "start_failed"))
401
+ assert account_broker.main(("choose", "--product", "codex")) == 0
402
+ assert json.loads(capsys.readouterr().out)["decision"]["account"] == "first"
403
+
404
+
405
+ def test_should_stop_after_timeout_without_running_job_again(
406
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
407
+ ) -> None:
408
+ accounts = (_account("first"), _account("second"))
409
+ monkeypatch.setitem(account_broker.all_product_adapters, Product.CODEX, _adapter(accounts, {
410
+ "first": _meters(80, 80), "second": _meters(70, 70)
411
+ }))
412
+ invoked_homes: list[str] = []
413
+
414
+ def runner(argv: object, **options: object) -> subprocess.CompletedProcess[str]:
415
+ invoked_homes.append(Path(options["env"]["CODEX_HOME"]).name)
416
+ raise subprocess.TimeoutExpired("job", 1)
417
+
418
+ with account_broker.override_subprocess_runner(runner):
419
+ outcome, report = account_broker._execute(Product.CODEX, ("job",), now=datetime.now(timezone.utc))
420
+
421
+ assert invoked_homes == ["first"]
422
+ assert outcome.attempts == (("first", "timeout"),)
423
+ assert outcome.status == "advisor_blocked"
424
+ assert outcome.account_name == "first"
425
+ assert report.final_decision.account.name == "first"
383
426
  assert account_broker.main(("choose", "--product", "codex")) == 0
384
427
  assert json.loads(capsys.readouterr().out)["decision"]["account"] == "first"
385
428