claude-dev-env 8.69.0 → 8.70.0

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 (99) hide show
  1. package/.agents/skills/correction/SKILL.md +28 -6
  2. package/.agents/skills/correction/references/filing.md +33 -0
  3. package/.agents/skills/correction/scripts/correction_filing.py +327 -0
  4. package/.agents/skills/correction/scripts/correction_filing_constants/__init__.py +0 -0
  5. package/.agents/skills/correction/scripts/correction_filing_constants/config/__init__.py +0 -0
  6. package/.agents/skills/correction/scripts/correction_filing_constants/config/constants.py +56 -0
  7. package/.agents/skills/correction/scripts/pytest.ini +4 -0
  8. package/.agents/skills/correction/scripts/test_correction_filing.py +155 -0
  9. package/.agents/skills/pr-lifecycle/SKILL.md +3 -1
  10. package/.agents/skills/second-account-workers/SKILL.md +21 -3
  11. package/docs/rule-guides/features-start-with-an-eval.md +40 -0
  12. package/hooks/blocking/followup_pr_dedupe.py +55 -5
  13. package/hooks/blocking/image_read_size_gate.py +167 -0
  14. package/hooks/blocking/pr_lifecycle_skill_gate.py +62 -11
  15. package/hooks/blocking/pull_request_proof.py +86 -20
  16. package/hooks/blocking/test_artifact_look_gate.py +1 -0
  17. package/hooks/blocking/test_followup_pr_dedupe.py +33 -0
  18. package/hooks/blocking/test_image_read_size_gate.py +79 -0
  19. package/hooks/blocking/test_pr_lifecycle_skill_gate.py +117 -3
  20. package/hooks/blocking/test_pre_tool_use_dispatcher.py +1 -5
  21. package/hooks/blocking/test_pull_request_proof.py +124 -1
  22. package/hooks/features/README.md +1 -0
  23. package/hooks/features/conduct-gates.md +1 -1
  24. package/hooks/features/read-gates.md +20 -0
  25. package/hooks/features/write-edit-blocking.md +3 -5
  26. package/hooks/hooks.json +11 -1
  27. package/hooks/hooks_constants/artifact_look_gate_constants.py +1 -2
  28. package/hooks/hooks_constants/followup_pr_dedupe_constants.py +4 -0
  29. package/hooks/hooks_constants/image_read_size_gate_constants.py +41 -0
  30. package/hooks/hooks_constants/pr_lifecycle_skill_gate_constants.py +3 -0
  31. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -5
  32. package/hooks/hooks_constants/pull_request_proof_constants.py +20 -1
  33. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +4 -11
  34. package/hooks/hooks_constants/transcript_skill_scan_constants.py +1 -0
  35. package/hooks/test_transcript_skill_scan.py +75 -1
  36. package/hooks/transcript_skill_scan.py +87 -15
  37. package/package.json +1 -1
  38. package/rules/anti-corollary-tests.md +2 -2
  39. package/rules/archiving-agent-config.md +2 -2
  40. package/rules/asd-ste100-language.md +2 -2
  41. package/rules/bdd.md +1 -1
  42. package/rules/build-needs-a-user.md +1 -1
  43. package/rules/cleanup-temp-files.md +3 -3
  44. package/rules/code-standards.md +1 -1
  45. package/rules/correction-lens.md +3 -3
  46. package/rules/destructive-commands.md +3 -3
  47. package/rules/doc-inventory-integrity.md +1 -1
  48. package/rules/docstring-prose-matches-implementation.md +1 -1
  49. package/rules/explore-thoroughly.md +1 -1
  50. package/rules/failure-blast-radius.md +1 -1
  51. package/rules/falsify-before-green.md +2 -2
  52. package/rules/features-start-with-an-eval.md +9 -0
  53. package/rules/filesystem-search.md +3 -3
  54. package/rules/flag-non-breaking-findings.md +2 -2
  55. package/rules/long-horizon-autonomy.md +2 -2
  56. package/rules/memory-stores-durable-facts.md +2 -2
  57. package/rules/no-contrast-framing.md +1 -1
  58. package/rules/no-cross-skill-duplicate-helpers.md +1 -1
  59. package/rules/orphan-css-class.md +1 -1
  60. package/rules/paired-test-coverage.md +1 -1
  61. package/rules/plain-illustrative-docstrings.md +1 -1
  62. package/rules/prefer-existing-tools.md +2 -2
  63. package/rules/proof-before-pull-request.md +1 -1
  64. package/rules/request-scope.md +1 -1
  65. package/rules/research-mode.md +2 -2
  66. package/rules/retired-hook-prose.md +1 -1
  67. package/rules/session-title.md +1 -1
  68. package/rules/shell-invocation.md +2 -2
  69. package/rules/testing.md +1 -1
  70. package/rules/verify-before-asking.md +3 -3
  71. package/rules/verify-runtime-state.md +2 -2
  72. package/rules/windows-filesystem-safe.md +1 -1
  73. package/rules/workers-done-before-complete.md +2 -2
  74. package/rules/workflow-substitution-slots.md +1 -1
  75. package/scripts/account_broker.py +10 -3
  76. package/scripts/account_broker_support.py +7 -3
  77. package/scripts/agent_image_copy.py +155 -0
  78. package/scripts/claude_account_worker.py +24 -5
  79. package/scripts/claude_account_worker_process.py +5 -0
  80. package/scripts/claude_account_worker_report.py +16 -1
  81. package/scripts/dev_env_scripts_constants/agent_image_copy_constants.py +11 -0
  82. package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +12 -0
  83. package/scripts/policy_lint/config/constants.py +1 -0
  84. package/scripts/test_account_broker.py +38 -0
  85. package/scripts/test_account_broker_support.py +24 -0
  86. package/scripts/test_agent_image_copy.py +76 -0
  87. package/scripts/test_claude_account_worker.py +47 -0
  88. package/scripts/test_claude_account_worker_process.py +20 -0
  89. package/scripts/test_claude_account_worker_report.py +15 -0
  90. package/scripts/tests/test_policy_lint_rules_retired_hook_prose.py +4 -4
  91. package/scripts/tests/test_rule_load_scopes.py +1 -0
  92. package/hooks/advisory/conftest.py +0 -10
  93. package/hooks/advisory/refactor_guard.py +0 -385
  94. package/hooks/advisory/refactor_guard_test_support.py +0 -46
  95. package/hooks/advisory/test_refactor_guard.py +0 -148
  96. package/hooks/advisory/test_refactor_guard_advisory.py +0 -242
  97. package/hooks/advisory/test_refactor_guard_eligibility.py +0 -166
  98. package/hooks/hooks_constants/refactor_guard_constants.py +0 -77
  99. package/hooks/hooks_constants/test_refactor_guard_constants.py +0 -21
@@ -909,6 +909,26 @@ def test_should_keep_a_spent_mark_without_a_reset_for_one_hour(
909
909
  assert decision["resets_at"] == (NOW + timedelta(hours=1)).isoformat()
910
910
 
911
911
 
912
+ def test_should_mark_a_windows_launcher_path_account_spent(
913
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
914
+ ) -> None:
915
+ launcher_name = "C:\\Users\\someone\\.local\\bin\\claude-first.cmd"
916
+ first = _account(launcher_name)
917
+ second = _account("second")
918
+ monkeypatch.setitem(
919
+ account_broker.all_product_adapters,
920
+ Product.CODEX,
921
+ _adapter((first, second), {launcher_name: _meters(80, 80), "second": _meters(80, 80)}),
922
+ )
923
+ _freeze_clock(monkeypatch, NOW)
924
+
925
+ code = account_broker.main(("choose", "--product", "codex", "--spent", f"{launcher_name}:{(NOW + timedelta(hours=2)).timestamp()}"))
926
+
927
+ decision = json.loads(capsys.readouterr().out)["decision"]
928
+ assert code == 0
929
+ assert decision["account"] == "second"
930
+
931
+
912
932
  def test_should_reject_an_invalid_spent_reset(
913
933
  monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
914
934
  ) -> None:
@@ -1194,3 +1214,21 @@ def test_should_keep_the_parent_environment_for_codex_jobs(
1194
1214
  run_job(Product.CODEX, ("codex", "exec", "task"))
1195
1215
 
1196
1216
  assert all_child_environments[0]["CLAUDE_CODE_SESSION_ID"] == "parent-session"
1217
+
1218
+
1219
+
1220
+ def test_should_hand_the_live_log_to_the_subprocess_runner(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
1221
+ live_log = tmp_path / "events.jsonl"
1222
+ adapter = _adapter((_account("only"),), {"only": _meters(80, 80)})
1223
+ captured_options: dict[str, object] = {}
1224
+
1225
+ def runner(command: object, **options: object) -> subprocess.CompletedProcess[str]:
1226
+ captured_options.update(options)
1227
+ return subprocess.CompletedProcess(command, 0, "served", "")
1228
+
1229
+ monkeypatch.setitem(account_broker.all_product_adapters, Product.CODEX, adapter)
1230
+ with account_broker.override_subprocess_runner(runner):
1231
+ outcome, _ = account_broker._execute(Product.CODEX, ("job",), now=NOW, live_log=live_log)
1232
+
1233
+ assert outcome.returncode == 0
1234
+ assert captured_options["live_log"] == live_log
@@ -419,3 +419,27 @@ def test_should_pass_metacharacters_to_an_executable_unchanged(monkeypatch: pyte
419
419
  support.subprocess_runner(["claude", "-p", 'say "a & b" | 100%!'])
420
420
 
421
421
  assert all_launched_argv == [[resolved_command, "-p", 'say "a & b" | 100%!']]
422
+
423
+
424
+ def test_should_write_child_output_into_the_live_log_while_it_runs(tmp_path: Path) -> None:
425
+ live_log = tmp_path / "worker.jsonl"
426
+ child_script = "import sys, pathlib; print('first', flush=True); sys.stdout.write(pathlib.Path(sys.argv[1]).read_text())"
427
+
428
+ completed = support._run_captured_subprocess([sys.executable, "-c", child_script, str(live_log)], live_log=live_log)
429
+
430
+ assert completed.stdout == "first\nfirst\n"
431
+ assert live_log.read_text(encoding="utf-8") == "first\nfirst\n"
432
+
433
+
434
+ def test_should_match_order_names_to_an_account_named_by_its_windows_launcher_path() -> None:
435
+ launcher_account = Account(
436
+ Product.CLAUDE,
437
+ "C:\\Users\\someone\\.local\\bin\\claude-editor.cmd",
438
+ Path("/profiles/editor"),
439
+ False,
440
+ "C:\\Users\\someone\\.local\\bin\\claude-editor.cmd",
441
+ )
442
+
443
+ ranked = support._with_priorities((launcher_account,), ("claude-org-jon", "claude-editor"))
444
+
445
+ assert ranked[0].priority == 1
@@ -0,0 +1,76 @@
1
+ """Behavior tests for the agent image copy script."""
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+ from PIL import Image
9
+
10
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
11
+
12
+ import agent_image_copy
13
+
14
+
15
+ def test_capped_size_should_keep_the_aspect_ratio() -> None:
16
+ assert agent_image_copy.capped_size(1440, 2560) == (288, 512)
17
+ assert agent_image_copy.capped_size(2000, 1000) == (512, 256)
18
+ assert agent_image_copy.capped_size(300, 200) == (300, 200)
19
+
20
+
21
+ def test_visual_tokens_should_count_28_pixel_patches() -> None:
22
+ assert agent_image_copy.visual_tokens(1440, 2560) == 4784
23
+ assert agent_image_copy.visual_tokens(288, 512) == 209
24
+
25
+
26
+ def test_parse_crop_box_should_refuse_three_numbers() -> None:
27
+ assert agent_image_copy.parse_crop_box("0,0,10,20") == (0, 0, 10, 20)
28
+ with pytest.raises(argparse.ArgumentTypeError):
29
+ agent_image_copy.parse_crop_box("0,0,10")
30
+
31
+
32
+ def test_write_agent_copy_should_write_a_capped_newer_file(tmp_path: Path) -> None:
33
+ source_path = tmp_path / "preview.png"
34
+ Image.new("RGB", (1440, 2560), "white").save(source_path)
35
+ copy_path = agent_image_copy.write_agent_copy(source_path, None)
36
+ assert copy_path.name == "preview.agent.png"
37
+ with Image.open(copy_path) as copy_image:
38
+ assert copy_image.size == (288, 512)
39
+ assert copy_path.stat().st_mtime >= source_path.stat().st_mtime
40
+
41
+
42
+ def test_write_agent_copy_should_crop_before_scaling(tmp_path: Path) -> None:
43
+ source_path = tmp_path / "preview.png"
44
+ Image.new("RGB", (1440, 2560), "white").save(source_path)
45
+ copy_path = agent_image_copy.write_agent_copy(source_path, (0, 0, 720, 640))
46
+ with Image.open(copy_path) as copy_image:
47
+ assert copy_image.size == (512, 455)
48
+
49
+
50
+ def test_write_agent_copy_should_write_a_cmyk_jpeg_as_rgb(tmp_path: Path) -> None:
51
+ source_path = tmp_path / "print.jpg"
52
+ Image.new("CMYK", (1000, 800)).save(source_path)
53
+ copy_path = agent_image_copy.write_agent_copy(source_path, None)
54
+ with Image.open(copy_path) as copy_image:
55
+ assert copy_image.mode == "RGB"
56
+ assert copy_image.size == (512, 410)
57
+
58
+
59
+ def test_write_agent_copy_should_follow_the_exif_orientation(tmp_path: Path) -> None:
60
+ source_path = tmp_path / "phone.jpg"
61
+ source_image = Image.new("RGB", (1000, 600), "white")
62
+ all_exif_tags = source_image.getexif()
63
+ all_exif_tags[0x0112] = 6
64
+ source_image.save(source_path, exif=all_exif_tags)
65
+ copy_path = agent_image_copy.write_agent_copy(source_path, None)
66
+ with Image.open(copy_path) as copy_image:
67
+ assert copy_image.size == (307, 512)
68
+
69
+
70
+ def test_main_should_print_the_copy_path(
71
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
72
+ ) -> None:
73
+ source_path = tmp_path / "shot.png"
74
+ Image.new("RGB", (1600, 900), "white").save(source_path)
75
+ assert agent_image_copy.main([str(source_path)]) == 0
76
+ assert "shot.agent.png 512x288 about 209 tokens" in capsys.readouterr().out
@@ -45,6 +45,7 @@ def _run_worker(
45
45
  *,
46
46
  model: str | None = None,
47
47
  permission_mode: str = "auto",
48
+ live_log: Path | None = None,
48
49
  ) -> tuple[int, dict[str, object], dict[str, object]]:
49
50
  prompt_file = tmp_path / "brief.md"
50
51
  prompt_file.write_text("standalone brief", encoding="utf-8")
@@ -65,6 +66,7 @@ def _run_worker(
65
66
  model=model,
66
67
  permission_mode=permission_mode,
67
68
  timeout_minutes=60,
69
+ live_log=live_log,
68
70
  )
69
71
  return exit_code, json.loads(report_file.read_text(encoding="utf-8")), captured
70
72
 
@@ -88,6 +90,51 @@ def test_should_send_prompt_and_flags_to_broker(
88
90
  assert captured["timeout_seconds"] == 3600
89
91
 
90
92
 
93
+ def test_should_stream_events_into_the_live_log_when_one_is_named(
94
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
95
+ ) -> None:
96
+ live_log = tmp_path / "worker.jsonl"
97
+
98
+ exit_code, report, captured = _run_worker(monkeypatch, tmp_path, _outcome(), live_log=live_log)
99
+
100
+ assert exit_code == 0
101
+ assert report["result"] == "ready"
102
+ assert captured["argv"][:5] == ["claude", "-p", "--output-format", "stream-json", "--verbose"]
103
+ assert captured["live_log"] == live_log
104
+
105
+
106
+ def test_should_pass_no_live_log_when_none_is_named(
107
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
108
+ ) -> None:
109
+ _, _, captured = _run_worker(monkeypatch, tmp_path, _outcome())
110
+
111
+ assert "live_log" not in captured
112
+
113
+
114
+ def test_should_report_an_unwritable_live_log_before_the_broker_runs(
115
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
116
+ ) -> None:
117
+ prompt_file = tmp_path / "brief.md"
118
+ prompt_file.write_text("standalone brief", encoding="utf-8")
119
+ report_file = tmp_path / "report.json"
120
+ monkeypatch.setattr(worker, "worker_job_runner", lambda *args, **kwargs: pytest.fail("broker ran"))
121
+
122
+ exit_code = worker.run_worker(
123
+ prompt_file=prompt_file,
124
+ cwd=tmp_path,
125
+ report_file=report_file,
126
+ model=None,
127
+ permission_mode="auto",
128
+ timeout_minutes=60,
129
+ live_log=tmp_path / "missing" / "worker.jsonl",
130
+ )
131
+
132
+ report = json.loads(report_file.read_text(encoding="utf-8"))
133
+ assert exit_code == report["exit_code"] == 1
134
+ assert report["account"] == "none"
135
+ assert report["reason"] == "live log file is unwritable"
136
+
137
+
91
138
  def test_should_report_the_broker_wait_reason_and_exit_three(
92
139
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
93
140
  ) -> None:
@@ -45,3 +45,23 @@ def test_should_send_job_to_runner_and_return_outcome_with_elapsed_seconds(
45
45
  },
46
46
  )
47
47
  ]
48
+
49
+
50
+ def test_should_pass_the_live_log_to_the_runner_when_one_is_named(tmp_path: Path) -> None:
51
+ live_log = tmp_path / "events.jsonl"
52
+ captured_options: dict[str, object] = {}
53
+
54
+ def recording_runner(product: Product, argv: list[str], **options: object) -> JobOutcome:
55
+ captured_options.update(options)
56
+ return JobOutcome(0, "{}", "", "main", (), "served", None, None)
57
+
58
+ invoke_worker(
59
+ all_arguments=["claude", "-p"],
60
+ cwd=tmp_path,
61
+ prompt_text="brief",
62
+ timeout_minutes=1,
63
+ runner=recording_runner,
64
+ live_log=live_log,
65
+ )
66
+
67
+ assert captured_options["live_log"] == live_log
@@ -51,6 +51,21 @@ def test_should_build_report_from_json_result_and_round_duration() -> None:
51
51
  assert report.wait_reset_at is None
52
52
 
53
53
 
54
+ def test_should_take_the_result_event_from_streamed_output() -> None:
55
+ streamed_lines = "\n".join(
56
+ (
57
+ '{"type":"system","subtype":"init","session_id":"s1"}',
58
+ '{"type":"assistant","message":{"content":[]}}',
59
+ '{"type":"result","result":"ready","is_error":false}',
60
+ )
61
+ )
62
+
63
+ report = make_report("extra_2", "served", exit_code=0, duration_seconds=1.0, stdout_text=streamed_lines)
64
+
65
+ assert report.payload == "ready"
66
+ assert report.is_error is False
67
+
68
+
54
69
  def test_should_mark_report_as_error_when_json_flags_error() -> None:
55
70
  report = make_report(
56
71
  "extra_2",
@@ -25,7 +25,7 @@ _HOOKS_CONFIGURATION = """
25
25
  """
26
26
  _DISPATCHER_ROSTER_SOURCE = """
27
27
  ALL_HOSTED_HOOK_ENTRIES = (
28
- HostedHookEntry("advisory/refactor_guard.py", is_blocking=False),
28
+ HostedHookEntry("advisory/migration_safety_advisor.py", is_blocking=False),
29
29
  )
30
30
  """
31
31
 
@@ -40,7 +40,7 @@ def _build_package(repository_root: Path) -> None:
40
40
  (package_root / "bin").mkdir(parents=True)
41
41
  (package_root / "rules").mkdir(parents=True)
42
42
  (hooks_root / "blocking" / "windows_rmtree_blocker.py").write_text("", "utf-8")
43
- (hooks_root / "advisory" / "refactor_guard.py").write_text("", "utf-8")
43
+ (hooks_root / "advisory" / "migration_safety_advisor.py").write_text("", "utf-8")
44
44
  (hooks_root / "observability" / "session_file_edit_tracker.py").write_text(
45
45
  "", "utf-8"
46
46
  )
@@ -96,7 +96,7 @@ def test_a_registered_hook_described_in_the_present_tense_passes(
96
96
  _build_package(tmp_path)
97
97
  all_messages = _diagnostics_for(
98
98
  tmp_path,
99
- "`session_file_edit_tracker` records each Write path, and `refactor_guard` reports a risk.\n",
99
+ "`session_file_edit_tracker` records each Write path, and `migration_safety_advisor` reports a risk.\n",
100
100
  )
101
101
  assert all_messages == ()
102
102
 
@@ -156,7 +156,7 @@ def test_a_second_hook_name_bounds_the_first_claim(tmp_path: Path) -> None:
156
156
  _build_package(tmp_path)
157
157
  all_messages = _diagnostics_for(
158
158
  tmp_path,
159
- "`destructive_command_blocker` was retired, and `refactor_guard` reports a risk.",
159
+ "`destructive_command_blocker` was retired, and `migration_safety_advisor` reports a risk.",
160
160
  )
161
161
  assert all_messages == ()
162
162
 
@@ -13,6 +13,7 @@ ALWAYS_ON_RULE_NAMES = frozenset(
13
13
  "correction-lens.md",
14
14
  "destructive-commands.md",
15
15
  "explore-thoroughly.md",
16
+ "features-start-with-an-eval.md",
16
17
  "filesystem-search.md",
17
18
  "memory-stores-durable-facts.md",
18
19
  "no-contrast-framing.md",
@@ -1,10 +0,0 @@
1
- """Pytest registration for shared refactor guard test support."""
2
-
3
- import sys
4
- from pathlib import Path
5
-
6
- advisory_directory = str(Path(__file__).resolve().parent)
7
- if advisory_directory not in sys.path:
8
- sys.path.insert(0, advisory_directory)
9
-
10
- from refactor_guard_test_support import git_repository # noqa: E402, F401