claude-dev-env 8.82.1 → 8.83.1

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.
Files changed (66) hide show
  1. package/.agents/skills/deepen/SKILL.md +42 -0
  2. package/bin/codex-skill-load-block.mjs +32 -0
  3. package/bin/codex-skill-load-block.test.mjs +27 -0
  4. package/bin/codex-trimmed-sol.test.mjs +1 -0
  5. package/bin/ever-shipped-skills.mjs +1 -0
  6. package/bin/install-pstack-plugin.mjs +12 -0
  7. package/bin/install-pstack-plugin.test.mjs +23 -0
  8. package/bin/install.agents-home.test.mjs +1 -0
  9. package/bin/install.codex-rules.test.mjs +2 -0
  10. package/bin/install.cursor-rules.test.mjs +38 -1
  11. package/bin/install.mjs +34 -2
  12. package/bin/install.prompt-context.test.mjs +3 -1
  13. package/bin/install.prune.test.mjs +1 -0
  14. package/bin/install.question-presentation.test.mjs +4 -0
  15. package/bin/install.settings-defaults.test.mjs +1 -0
  16. package/bin/install.shared-settings.test.mjs +1 -0
  17. package/bin/install.test.mjs +34 -1
  18. package/bin/install.transaction.test.mjs +8 -0
  19. package/bin/install.uninstall-transaction.test.mjs +2 -0
  20. package/bin/install.update-keeps-hooks.test.mjs +1 -0
  21. package/commands/deepen.md +19 -0
  22. package/docs/account-broker.md +1 -1
  23. package/docs/rule-guides/agent-board.md +27 -0
  24. package/docs/rule-guides/features-start-with-an-eval.md +4 -2
  25. package/docs/rule-guides/proof-before-pull-request.md +1 -1
  26. package/hooks/blocking/build_eval_before_eval_write_gate.py +102 -0
  27. package/hooks/blocking/pr_lifecycle_skill_gate.py +42 -16
  28. package/hooks/blocking/pull_request_proof.py +111 -2
  29. package/hooks/blocking/reply_length_gate.py +69 -13
  30. package/hooks/blocking/test_build_eval_before_eval_write_gate.py +162 -0
  31. package/hooks/blocking/test_pr_lifecycle_skill_gate.py +25 -0
  32. package/hooks/blocking/test_pull_request_proof.py +115 -0
  33. package/hooks/blocking/test_reply_length_gate.py +98 -10
  34. package/hooks/context_budget/tests/test_model.py +65 -0
  35. package/hooks/features/conduct-gates.md +4 -0
  36. package/hooks/features/lifecycle-cleanup.md +1 -1
  37. package/hooks/features/session-context.md +3 -0
  38. package/hooks/hooks.json +20 -0
  39. package/hooks/hooks_constants/build_eval_before_eval_write_gate_constants.py +19 -0
  40. package/hooks/hooks_constants/deepen_first_prompt_constants.py +119 -0
  41. package/hooks/hooks_constants/mod_handoff_constants.py +2 -0
  42. package/hooks/hooks_constants/nested_project_hooks_constants.py +2 -0
  43. package/hooks/hooks_constants/pull_request_proof_constants.py +38 -0
  44. package/hooks/hooks_constants/reply_length_gate_constants.py +14 -0
  45. package/hooks/hooks_constants/test_reply_length_gate_constants.py +8 -0
  46. package/hooks/lifecycle/nested_project_hooks.py +17 -8
  47. package/hooks/lifecycle/test_nested_project_hooks.py +50 -0
  48. package/hooks/session/deepen_first_prompt.py +390 -0
  49. package/hooks/session/test_deepen_first_prompt.py +274 -0
  50. package/hooks/test_transcript_skill_scan.py +6 -2
  51. package/hooks/transcript_skill_scan.py +18 -7
  52. package/package.json +1 -1
  53. package/rules/agent-board.md +9 -0
  54. package/rules/index.md +1 -1
  55. package/scripts/account_broker.py +1 -0
  56. package/scripts/account_broker_support.py +71 -24
  57. package/scripts/ci/install-playtest.mjs +329 -2
  58. package/scripts/ci/install-playtest.test.mjs +6 -0
  59. package/scripts/dev_env_scripts_constants/account_broker_constants.py +11 -3
  60. package/scripts/dev_env_scripts_constants/test_account_broker_constants.py +15 -0
  61. package/scripts/policy_lint/adapter_configuration.py +5 -0
  62. package/scripts/policy_lint/config/constants.py +1 -0
  63. package/scripts/test_account_broker.py +19 -1
  64. package/scripts/test_account_broker_support.py +161 -3
  65. package/scripts/tests/test_adapter_configuration.py +8 -0
  66. package/scripts/tests/test_rule_load_scopes.py +1 -0
@@ -191,6 +191,21 @@ def test_should_treat_accounts_that_differ_only_in_profile_as_one_account() -> N
191
191
  assert Account(Product.CLAUDE, "first", home, profile="first") == Account(Product.CLAUDE, "first", home)
192
192
 
193
193
 
194
+ def test_should_wait_until_a_rate_limited_meter_may_be_probed_again() -> None:
195
+ now = datetime(2026, 10, 11, 19, 31, tzinfo=timezone.utc)
196
+ probe_again_at = now + timedelta(minutes=5)
197
+ account = Account(Product.CLAUDE, "second", Path("second"))
198
+ reading = broker_constants.Reading(account, None, "usage probe failed: HTTP Error 429", None, probe_again_at)
199
+
200
+ decision = broker_constants._wait_decision([reading], now, frozenset(), {})
201
+
202
+ assert decision.resets_at == probe_again_at
203
+ assert decision.reason == (
204
+ "no account meter could be read (second (usage probe failed: HTTP Error 429)); "
205
+ "next check at 2026-10-11T19:36:00+00:00"
206
+ )
207
+
208
+
194
209
  def _codex_reading(meters: broker_constants.Meters) -> broker_constants.Reading:
195
210
  return broker_constants.Reading(Account(Product.CODEX, "codex-1", Path("/profiles/codex-1")), meters)
196
211
 
@@ -135,6 +135,7 @@ def _names_exempt_registration_path(registered_string: str) -> bool:
135
135
  hooks/blocking/verify_before_acting.py -> exempt
136
136
  hooks/blocking/session_title_format_gate.py -> exempt
137
137
  hooks/blocking/artifact_dark_mode_gate.py -> exempt
138
+ hooks/blocking/build_eval_before_eval_write_gate.py -> exempt
138
139
  hooks/blocking/merge_over_review_offer_gate.py -> exempt
139
140
  hooks/blocking/final_reply_gate.py -> exempt
140
141
  hooks/blocking/some_new_blocker.py -> flagged
@@ -168,6 +169,10 @@ def _names_exempt_registration_path(registered_string: str) -> bool:
168
169
  The artifact page gate denies a page publish whose text is unreadable in
169
170
  dark mode. It decides how a page renders and no code or safety policy.
170
171
 
172
+ The eval write gate denies a write to an eval file until the session
173
+ invokes ``/claude-api build-eval``. It holds eval writes until a skill
174
+ loads, and it judges no code or safety content.
175
+
171
176
  The merge offer gate denies a decision card whose text offers a merge and
172
177
  names open review findings. It reads only the wording of a chat card and
173
178
  decides no code or safety policy; the merge review gate holds the merge.
@@ -74,6 +74,7 @@ ALL_ACTION_BOUNDARY_EXEMPT_REGISTRATION_PATHS = (
74
74
  "blocking/image_read_size_gate.py",
75
75
  "blocking/artifact_dark_mode_gate.py",
76
76
  "blocking/haiku_spawn_slim_gate.py",
77
+ "blocking/build_eval_before_eval_write_gate.py",
77
78
  "blocking/merge_over_review_offer_gate.py",
78
79
  "blocking/final_reply_gate.py",
79
80
  )
@@ -470,12 +470,30 @@ def test_should_count_failed_meter_read_as_spent() -> None:
470
470
 
471
471
 
472
472
  def test_should_serialize_readings_with_named_fields() -> None:
473
- reading = Reading(_account("one"), _meters(80, 70))
473
+ reading = Reading(_account("one"), _meters(80, 70), measured_at=NOW)
474
474
 
475
475
  payload = account_broker.readings_payload((reading,))
476
476
 
477
477
  assert payload[0]["name"] == "one"
478
478
  assert payload[0]["meters"]["weekly_percent_left"] == 70
479
+ assert payload[0]["measured_at"] == NOW.isoformat()
480
+
481
+
482
+ def test_should_wait_until_the_rate_limit_probe_after_stale_meters_expire() -> None:
483
+ reading = Reading(
484
+ _account("one", Product.CLAUDE), None,
485
+ "usage probe failed: HTTP Error 429", NOW - timedelta(minutes=25),
486
+ NOW + timedelta(minutes=5),
487
+ )
488
+
489
+ decision = choose_from_readings(Product.CLAUDE, (reading,), now=NOW)
490
+
491
+ assert decision.action == "wait"
492
+ assert decision.resets_at == NOW + timedelta(minutes=5)
493
+ assert decision.reason == (
494
+ "no account meter could be read (one (usage probe failed: HTTP Error 429)); "
495
+ "next check at 2026-10-03T00:05:00+00:00"
496
+ )
479
497
 
480
498
 
481
499
  def test_should_wait_when_all_meter_reads_fail() -> None:
@@ -7,7 +7,7 @@ import shutil
7
7
  import subprocess
8
8
  import sys
9
9
  import time
10
- from datetime import datetime, timezone
10
+ from datetime import datetime, timedelta, timezone
11
11
  from collections.abc import Callable
12
12
  from pathlib import Path
13
13
  from types import SimpleNamespace
@@ -341,10 +341,11 @@ def test_should_cap_the_meter_read_delay(monkeypatch: pytest.MonkeyPatch, tmp_pa
341
341
  all_failures = [support.WeeklyUtilizationProbeError("down") for _ in range(6)]
342
342
  _claude_meter_reads(monkeypatch, all_failures)
343
343
 
344
- meters, unread_reason = support.read_meters_with_backoff(support.read_claude_meters, Account(Product.CLAUDE, "one", tmp_path))
344
+ meters, unread_reason, is_rate_limited = support.read_meters_with_backoff(support.read_claude_meters, Account(Product.CLAUDE, "one", tmp_path))
345
345
 
346
346
  assert meters is None
347
347
  assert unread_reason == "down"
348
+ assert not is_rate_limited
348
349
  assert all_delays == [1.0, 2.0, 4.0, 8.0, 8.0]
349
350
 
350
351
 
@@ -356,10 +357,11 @@ def test_should_read_a_signed_out_claude_account_once(monkeypatch: pytest.Monkey
356
357
  all_reads.append(account)
357
358
  raise support.ClaudeSignedOutError("no usable bearer token")
358
359
 
359
- meters, unread_reason = support.read_meters_with_backoff(signed_out_reader, Account(Product.CLAUDE, "main", tmp_path))
360
+ meters, unread_reason, is_rate_limited = support.read_meters_with_backoff(signed_out_reader, Account(Product.CLAUDE, "main", tmp_path))
360
361
 
361
362
  assert meters is None
362
363
  assert unread_reason == "no usable bearer token"
364
+ assert not is_rate_limited
363
365
  assert len(all_reads) == 1
364
366
  assert all_delays == []
365
367
 
@@ -385,6 +387,162 @@ def test_should_cache_account_reading_under_injected_state_path(
385
387
  assert support.broker_state_path().is_file()
386
388
 
387
389
 
390
+ def _read_twice(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, all_results: list[object], gap: timedelta) -> object:
391
+ monkeypatch.setattr(support, "broker_state_path", lambda: tmp_path / "broker" / "state.json")
392
+ _recorded_sleeps(monkeypatch)
393
+ account = Account(Product.CLAUDE, "second", tmp_path)
394
+ pending = list(all_results)
395
+
396
+ def reader(_selected: Account) -> Meters:
397
+ next_outcome = pending.pop(0)
398
+ if isinstance(next_outcome, Exception):
399
+ raise next_outcome
400
+ return next_outcome
401
+
402
+ adapter = ProductAdapter(lambda: (account,), reader, "CLAUDE_CONFIG_DIR", ())
403
+ state = support._load_state(support.broker_state_path())
404
+ first_read_at = datetime(2026, 10, 11, 19, 20, tzinfo=timezone.utc)
405
+ support.read_accounts(Product.CLAUDE, adapter, all_state=state, now=first_read_at)
406
+ return support.read_accounts(Product.CLAUDE, adapter, all_state=state, now=first_read_at + gap)[0]
407
+
408
+
409
+ def test_should_keep_the_last_good_meters_when_the_usage_endpoint_rate_limits(
410
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
411
+ ) -> None:
412
+ rate_limited = support.WeeklyUtilizationProbeError("usage probe failed: HTTP Error 429: Too Many Requests")
413
+ reading = _read_twice(monkeypatch, tmp_path, [Meters(74.0, None, 81.0, None), rate_limited], timedelta(minutes=5))
414
+
415
+ assert reading.meters == Meters(74.0, None, 81.0, None)
416
+ assert reading.unread_reason == str(rate_limited)
417
+ assert reading.measured_at == datetime(2026, 10, 11, 19, 20, tzinfo=timezone.utc)
418
+
419
+
420
+ def test_should_drop_last_good_meters_older_than_the_fallback_window(
421
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
422
+ ) -> None:
423
+ rate_limited = support.WeeklyUtilizationProbeError("usage probe failed: HTTP Error 429: Too Many Requests")
424
+ reading = _read_twice(monkeypatch, tmp_path, [Meters(74.0, None, 81.0, None), rate_limited], timedelta(hours=2))
425
+
426
+ assert reading.meters is None
427
+ assert reading.unread_reason == "usage probe failed: HTTP Error 429: Too Many Requests"
428
+
429
+
430
+ def test_should_keep_last_good_meters_at_the_fallback_boundary(
431
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
432
+ ) -> None:
433
+ rate_limited = support.WeeklyUtilizationProbeError("usage probe failed: HTTP Error 429")
434
+ reading = _read_twice(
435
+ monkeypatch, tmp_path,
436
+ [Meters(74.0, None, 81.0, None), rate_limited], timedelta(minutes=30),
437
+ )
438
+
439
+ assert reading.meters == Meters(74.0, None, 81.0, None)
440
+
441
+
442
+ def test_should_drop_last_good_meters_after_a_window_reset(
443
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
444
+ ) -> None:
445
+ first_read = datetime(2026, 10, 11, 19, 20, tzinfo=timezone.utc)
446
+ rate_limited = support.WeeklyUtilizationProbeError("usage probe failed: HTTP Error 429")
447
+ reading = _read_twice(
448
+ monkeypatch, tmp_path,
449
+ [Meters(3.0, first_read + timedelta(minutes=10), 81.0, None), rate_limited],
450
+ timedelta(minutes=25),
451
+ )
452
+
453
+ assert reading.meters is None
454
+ assert reading.unread_reason == str(rate_limited)
455
+
456
+
457
+ def test_should_drop_held_meters_when_a_window_resets(
458
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
459
+ ) -> None:
460
+ monkeypatch.setattr(support, "broker_state_path", lambda: tmp_path / "broker" / "state.json")
461
+ account = Account(Product.CLAUDE, "second", tmp_path)
462
+ start = datetime(2026, 10, 11, 19, 20, tzinfo=timezone.utc)
463
+ state = support._load_state(support.broker_state_path())
464
+ state["meters"][support._state_key(account)] = {
465
+ "read_at": start.timestamp(),
466
+ "measured_at": (start - timedelta(minutes=5)).timestamp(),
467
+ "meters": support._meter_payload(Meters(3.0, start + timedelta(minutes=1), 81.0, None)),
468
+ "unread_reason": "usage probe failed: HTTP Error 429",
469
+ "retry_at": (start + timedelta(minutes=5)).timestamp(),
470
+ }
471
+ adapter = ProductAdapter(lambda: (account,), lambda _: pytest.fail("probe during cooldown"), "CLAUDE_CONFIG_DIR", ())
472
+
473
+ reading = support.read_accounts(Product.CLAUDE, adapter, all_state=state, now=start + timedelta(minutes=2))[0]
474
+
475
+ assert reading.meters is None
476
+ assert reading.retry_at == start + timedelta(minutes=5)
477
+
478
+
479
+ def test_should_reject_a_last_good_entry_with_an_unrelated_failure() -> None:
480
+ now = datetime(2026, 10, 11, 19, 20, tzinfo=timezone.utc)
481
+ entry = {
482
+ "measured_at": now.timestamp(),
483
+ "meters": support._meter_payload(Meters(74.0, None, 81.0, None)),
484
+ "unread_reason": "credentials file missing",
485
+ }
486
+
487
+ assert support._last_good_meters(entry, now) is None
488
+
489
+
490
+ def test_should_not_use_last_good_meters_after_a_file_failure(
491
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
492
+ ) -> None:
493
+ reading = _read_twice(
494
+ monkeypatch, tmp_path,
495
+ [Meters(74.0, None, 81.0, None), *[OSError("credentials file missing") for _ in range(3)]],
496
+ timedelta(minutes=5),
497
+ )
498
+
499
+ assert reading.meters is None
500
+ assert reading.unread_reason == "credentials file missing"
501
+
502
+
503
+ def test_should_keep_first_measurement_time_across_rate_limit_fallbacks(
504
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
505
+ ) -> None:
506
+ monkeypatch.setattr(support, "broker_state_path", lambda: tmp_path / "broker" / "state.json")
507
+ _recorded_sleeps(monkeypatch)
508
+ account = Account(Product.CLAUDE, "second", tmp_path)
509
+ rate_limited = support.WeeklyUtilizationProbeError("usage probe failed: HTTP Error 429")
510
+ pending = [Meters(74.0, None, 81.0, None), rate_limited, rate_limited]
511
+ calls = []
512
+
513
+ def reader(_selected: Account) -> Meters:
514
+ calls.append(1)
515
+ outcome = pending.pop(0)
516
+ if isinstance(outcome, Exception):
517
+ raise outcome
518
+ return outcome
519
+
520
+ adapter = ProductAdapter(lambda: (account,), reader, "CLAUDE_CONFIG_DIR", ())
521
+ state = support._load_state(support.broker_state_path())
522
+ first = datetime(2026, 10, 11, 19, 20, tzinfo=timezone.utc)
523
+ support.read_accounts(Product.CLAUDE, adapter, all_state=state, now=first)
524
+ second = support.read_accounts(Product.CLAUDE, adapter, all_state=state, now=first + timedelta(minutes=5))[0]
525
+ held = support.read_accounts(Product.CLAUDE, adapter, all_state=state, now=first + timedelta(minutes=6))[0]
526
+ third = support.read_accounts(Product.CLAUDE, adapter, all_state=state, now=first + timedelta(minutes=10))[0]
527
+
528
+ assert second.measured_at == first
529
+ assert held.measured_at == first
530
+ assert third.measured_at == first
531
+ assert len(calls) == 3
532
+
533
+
534
+ def test_should_keep_newer_measurement_when_stale_fallback_saves_last(tmp_path: Path) -> None:
535
+ state_path = tmp_path / "broker" / "state.json"
536
+ account = Account(Product.CLAUDE, "second", tmp_path)
537
+ key = support._state_key(account)
538
+ old = {"meters": {key: {"read_at": 40.0, "measured_at": 10.0, "meters": support._meter_payload(Meters(74.0, None, 81.0, None)), "unread_reason": "HTTP Error 429"}}, "spent": {}, "affinity": {}}
539
+ newer = {"meters": {key: {"read_at": 30.0, "measured_at": 30.0, "meters": support._meter_payload(Meters(5.0, None, 81.0, None)), "unread_reason": None}}, "spent": {}, "affinity": {}}
540
+ support._save_state(state_path, newer)
541
+ support._save_state(state_path, old)
542
+
543
+ assert support._load_state(state_path)["meters"][key]["meters"]["session_percent_left"] == 5.0
544
+
545
+
388
546
  def test_should_extract_session_id_and_restore_runner() -> None:
389
547
  original = support.subprocess_runner
390
548
 
@@ -130,6 +130,14 @@ def test_new_artifact_page_gate_registration_is_clean(tmp_path: Path) -> None:
130
130
  assert adapters.hook_configuration_diagnostics(current_document, tmp_path) == ()
131
131
 
132
132
 
133
+ def test_new_eval_write_gate_registration_is_clean(tmp_path: Path) -> None:
134
+ current_document = _hook_document(
135
+ ["hooks/blocking/build_eval_before_eval_write_gate.py"],
136
+ '{"hooks": {}}',
137
+ )
138
+ assert adapters.hook_configuration_diagnostics(current_document, tmp_path) == ()
139
+
140
+
133
141
  def test_new_blocker_in_staged_change_is_rejected(tmp_path: Path) -> None:
134
142
  current_document = _hook_document(
135
143
  ["hooks/blocking/new.py"],
@@ -8,6 +8,7 @@ SKILLS_DIRECTORY = PACKAGE_ROOT / ".agents" / "skills"
8
8
 
9
9
  ALWAYS_ON_RULE_NAMES = frozenset(
10
10
  {
11
+ "agent-board.md",
11
12
  "correction-lens.md",
12
13
  "index.md",
13
14
  "question-presentation.md",