claude-dev-env 8.105.0 → 8.105.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.
@@ -115,6 +115,17 @@ def test_should_block_the_stop_with_the_finding_when_the_turn_leaves_a_bug(
115
115
  assert "app" in str(stop_output["reason"])
116
116
 
117
117
 
118
+ def test_should_tell_the_agent_to_fix_a_finding_unless_the_user_asked_for_it(
119
+ project: Path, gate_environment: dict[str, str]
120
+ ) -> None:
121
+ run_hook(gate_environment, "UserPromptSubmit")
122
+ drop_retry(project)
123
+ block_reason = str(run_hook(gate_environment, "Stop")["reason"])
124
+ assert "Fix each one before you end the turn." in block_reason
125
+ assert "is not a request for the problem it causes" in block_reason
126
+ assert "quote those words in your reply" in block_reason
127
+
128
+
118
129
  def test_should_review_only_the_edits_made_after_the_prompt(
119
130
  project: Path, gate_environment: dict[str, str]
120
131
  ) -> None:
@@ -149,7 +149,10 @@ FINDING_LINE_SEPARATOR = "\n"
149
149
  REPOSITORY_FINDINGS_SEPARATOR = "\n\n"
150
150
  BLOCK_REASON_TEMPLATE = (
151
151
  "A fresh-context review of this turn's changes found these problems. "
152
- "Fix each one, or say in your reply why it is wrong, before you end the turn:\n\n"
152
+ "Fix each one before you end the turn. Code the user pasted or described is not a "
153
+ "request for the problem it causes. Keep a finding's behavior only when the user's own "
154
+ "words ask for that behavior, and quote those words in your reply. When a finding is "
155
+ "wrong, say in your reply why:\n\n"
153
156
  "{all_findings}"
154
157
  )
155
158
  TOO_MANY_FILES_DIAGNOSTIC_TEMPLATE = (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.105.0",
3
+ "version": "8.105.2",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -392,15 +392,27 @@ def _unreadable_text(reading: Reading) -> str:
392
392
  return f"{reading.account.name} ({reading.unread_reason})"
393
393
 
394
394
 
395
- def _wait_reason(all_unreadable_names: Sequence[str], is_every_account_unreadable: bool, soonest: _RetryTime) -> str:
395
+ def _room_text(reading: Reading, is_spent: bool) -> str:
396
+ if is_spent or reading.meters is None:
397
+ return f"{reading.account.name} spent"
398
+ percent_left = reading.meters.tightest_percent_left
399
+ if percent_left is None:
400
+ return f"{reading.account.name} percent left unread"
401
+ return f"{reading.account.name} {percent_left:g}% left"
402
+
403
+
404
+ def _wait_reason(
405
+ all_room_texts: Sequence[str], all_unreadable_names: Sequence[str], is_every_account_unreadable: bool, soonest: _RetryTime
406
+ ) -> str:
396
407
  next_event = "reset" if soonest.is_known_reset else "check"
397
408
  next_text = f"next {next_event} at {utc_time_text(soonest.at)}"
409
+ room_text = f" ({', '.join(all_room_texts)})" if all_room_texts else ""
398
410
  if not all_unreadable_names:
399
- return f"no account has room; {next_text}"
411
+ return f"no account has room{room_text}; {next_text}"
400
412
  unreadable_text = ", ".join(all_unreadable_names)
401
413
  if is_every_account_unreadable:
402
414
  return f"no account meter could be read ({unreadable_text}); {next_text}"
403
- return f"no readable account has room; meter unreadable for {unreadable_text}; {next_text}"
415
+ return f"no readable account has room{room_text}; meter unreadable for {unreadable_text}; {next_text}"
404
416
 
405
417
 
406
418
  def _wait_decision(
@@ -423,5 +435,11 @@ def _wait_decision(
423
435
  for each_reading in all_readings
424
436
  if each_reading.meters is None and each_reading.account not in all_spent_accounts
425
437
  ]
438
+ all_room_texts = [
439
+ _room_text(each_reading, each_reading.account in all_spent_accounts)
440
+ for each_reading in all_readings
441
+ if each_reading.meters is not None or each_reading.account in all_spent_accounts
442
+ ]
426
443
  is_every_account_unreadable = len(all_unreadable_names) == len(all_readings)
427
- return Decision("wait", None, soonest.at, _wait_reason(all_unreadable_names, is_every_account_unreadable, soonest), "wait")
444
+ reason = _wait_reason(all_room_texts, all_unreadable_names, is_every_account_unreadable, soonest)
445
+ return Decision("wait", None, soonest.at, reason, "wait")
@@ -91,7 +91,7 @@ def test_should_name_the_unread_account_and_its_reason_when_no_account_has_room(
91
91
 
92
92
  assert decision.action == "wait"
93
93
  assert decision.reason == (
94
- "no readable account has room; meter unreadable for codex-3 (codex app-server failed: timed out); "
94
+ "no readable account has room (codex-4 0% left); meter unreadable for codex-3 (codex app-server failed: timed out); "
95
95
  "next check at 2026-10-03T01:00:00+00:00"
96
96
  )
97
97
  assert payload[0]["unread_reason"] == "codex app-server failed: timed out"
@@ -542,21 +542,21 @@ _EV_CLAUDE = _account("ev", Product.CLAUDE)
542
542
  pytest.param(
543
543
  (Reading(_MAIN_CLAUDE, _meters(5, 50, short_reset=NOW + timedelta(minutes=30))), Reading(_EV_CLAUDE, None)),
544
544
  {},
545
- "no readable account has room; meter unreadable for ev; next reset at 2026-10-03T00:30:00+00:00",
545
+ "no readable account has room (main 5% left); meter unreadable for ev; next reset at 2026-10-03T00:30:00+00:00",
546
546
  NOW + timedelta(minutes=30),
547
547
  id="unreadable-beside-a-reset-before-the-check",
548
548
  ),
549
549
  pytest.param(
550
550
  (Reading(_MAIN_CLAUDE, _meters(5, 50, short_reset=NOW + timedelta(hours=2))), Reading(_EV_CLAUDE, None)),
551
551
  {},
552
- "no readable account has room; meter unreadable for ev; next check at 2026-10-03T01:00:00+00:00",
552
+ "no readable account has room (main 5% left); meter unreadable for ev; next check at 2026-10-03T01:00:00+00:00",
553
553
  NOW + timedelta(hours=1),
554
554
  id="unreadable-beside-a-reset-after-the-check",
555
555
  ),
556
556
  pytest.param(
557
557
  (Reading(_MAIN_CLAUDE, _meters(80, 80)), Reading(_EV_CLAUDE, None)),
558
558
  {_MAIN_CLAUDE: NOW + timedelta(minutes=30)},
559
- "no readable account has room; meter unreadable for ev; next reset at 2026-10-03T00:30:00+00:00",
559
+ "no readable account has room (main spent); meter unreadable for ev; next reset at 2026-10-03T00:30:00+00:00",
560
560
  NOW + timedelta(minutes=30),
561
561
  id="unreadable-beside-a-spent-account",
562
562
  ),
@@ -566,17 +566,31 @@ _EV_CLAUDE = _account("ev", Product.CLAUDE)
566
566
  Reading(_EV_CLAUDE, _meters(5, 80, short_reset=NOW + timedelta(hours=3))),
567
567
  ),
568
568
  {},
569
- "no account has room; next reset at 2026-10-03T02:00:00+00:00",
569
+ "no account has room (main 5% left, ev 5% left); next reset at 2026-10-03T02:00:00+00:00",
570
570
  NOW + timedelta(hours=2),
571
571
  id="every-meter-read-with-known-resets",
572
572
  ),
573
573
  pytest.param(
574
574
  (Reading(_MAIN_CLAUDE, Meters(5, None, 50, None)),),
575
575
  {},
576
- "no account has room; next check at 2026-10-03T01:00:00+00:00",
576
+ "no account has room (main 5% left); next check at 2026-10-03T01:00:00+00:00",
577
577
  NOW + timedelta(hours=1),
578
578
  id="every-meter-read-without-a-known-reset",
579
579
  ),
580
+ pytest.param(
581
+ (),
582
+ {},
583
+ "no account has room; next check at 2026-10-03T01:00:00+00:00",
584
+ NOW + timedelta(hours=1),
585
+ id="no-account-in-the-roster",
586
+ ),
587
+ pytest.param(
588
+ (Reading(_MAIN_CLAUDE, Meters(None, None, None, None)),),
589
+ {},
590
+ "no account has room (main percent left unread); next check at 2026-10-03T01:00:00+00:00",
591
+ NOW + timedelta(hours=1),
592
+ id="meter-read-without-a-percent",
593
+ ),
580
594
  ),
581
595
  )
582
596
  def test_should_name_what_the_broker_knows_in_the_wait_reason(