claude-dev-env 8.69.0 → 8.69.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 (82) hide show
  1. package/.agents/skills/pr-lifecycle/SKILL.md +3 -1
  2. package/.agents/skills/second-account-workers/SKILL.md +21 -3
  3. package/docs/rule-guides/features-start-with-an-eval.md +40 -0
  4. package/hooks/blocking/followup_pr_dedupe.py +55 -5
  5. package/hooks/blocking/pr_lifecycle_skill_gate.py +62 -11
  6. package/hooks/blocking/pull_request_proof.py +86 -20
  7. package/hooks/blocking/test_artifact_look_gate.py +1 -0
  8. package/hooks/blocking/test_followup_pr_dedupe.py +33 -0
  9. package/hooks/blocking/test_pr_lifecycle_skill_gate.py +117 -3
  10. package/hooks/blocking/test_pre_tool_use_dispatcher.py +1 -5
  11. package/hooks/blocking/test_pull_request_proof.py +124 -1
  12. package/hooks/features/conduct-gates.md +1 -1
  13. package/hooks/features/write-edit-blocking.md +3 -5
  14. package/hooks/hooks.json +1 -1
  15. package/hooks/hooks_constants/artifact_look_gate_constants.py +1 -2
  16. package/hooks/hooks_constants/followup_pr_dedupe_constants.py +4 -0
  17. package/hooks/hooks_constants/pr_lifecycle_skill_gate_constants.py +3 -0
  18. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -5
  19. package/hooks/hooks_constants/pull_request_proof_constants.py +20 -1
  20. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +4 -11
  21. package/hooks/hooks_constants/transcript_skill_scan_constants.py +1 -0
  22. package/hooks/test_transcript_skill_scan.py +75 -1
  23. package/hooks/transcript_skill_scan.py +87 -15
  24. package/package.json +1 -1
  25. package/rules/anti-corollary-tests.md +2 -2
  26. package/rules/archiving-agent-config.md +2 -2
  27. package/rules/asd-ste100-language.md +2 -2
  28. package/rules/bdd.md +1 -1
  29. package/rules/build-needs-a-user.md +1 -1
  30. package/rules/cleanup-temp-files.md +3 -3
  31. package/rules/code-standards.md +1 -1
  32. package/rules/correction-lens.md +3 -3
  33. package/rules/destructive-commands.md +3 -3
  34. package/rules/doc-inventory-integrity.md +1 -1
  35. package/rules/docstring-prose-matches-implementation.md +1 -1
  36. package/rules/explore-thoroughly.md +1 -1
  37. package/rules/failure-blast-radius.md +1 -1
  38. package/rules/falsify-before-green.md +2 -2
  39. package/rules/features-start-with-an-eval.md +9 -0
  40. package/rules/filesystem-search.md +3 -3
  41. package/rules/flag-non-breaking-findings.md +2 -2
  42. package/rules/long-horizon-autonomy.md +2 -2
  43. package/rules/memory-stores-durable-facts.md +2 -2
  44. package/rules/no-contrast-framing.md +1 -1
  45. package/rules/no-cross-skill-duplicate-helpers.md +1 -1
  46. package/rules/orphan-css-class.md +1 -1
  47. package/rules/paired-test-coverage.md +1 -1
  48. package/rules/plain-illustrative-docstrings.md +1 -1
  49. package/rules/prefer-existing-tools.md +2 -2
  50. package/rules/proof-before-pull-request.md +1 -1
  51. package/rules/request-scope.md +1 -1
  52. package/rules/research-mode.md +2 -2
  53. package/rules/retired-hook-prose.md +1 -1
  54. package/rules/session-title.md +1 -1
  55. package/rules/shell-invocation.md +2 -2
  56. package/rules/testing.md +1 -1
  57. package/rules/verify-before-asking.md +3 -3
  58. package/rules/verify-runtime-state.md +2 -2
  59. package/rules/windows-filesystem-safe.md +1 -1
  60. package/rules/workers-done-before-complete.md +2 -2
  61. package/rules/workflow-substitution-slots.md +1 -1
  62. package/scripts/account_broker.py +10 -3
  63. package/scripts/account_broker_support.py +7 -3
  64. package/scripts/claude_account_worker.py +24 -5
  65. package/scripts/claude_account_worker_process.py +5 -0
  66. package/scripts/claude_account_worker_report.py +16 -1
  67. package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +12 -0
  68. package/scripts/test_account_broker.py +38 -0
  69. package/scripts/test_account_broker_support.py +24 -0
  70. package/scripts/test_claude_account_worker.py +47 -0
  71. package/scripts/test_claude_account_worker_process.py +20 -0
  72. package/scripts/test_claude_account_worker_report.py +15 -0
  73. package/scripts/tests/test_policy_lint_rules_retired_hook_prose.py +4 -4
  74. package/scripts/tests/test_rule_load_scopes.py +1 -0
  75. package/hooks/advisory/conftest.py +0 -10
  76. package/hooks/advisory/refactor_guard.py +0 -385
  77. package/hooks/advisory/refactor_guard_test_support.py +0 -46
  78. package/hooks/advisory/test_refactor_guard.py +0 -148
  79. package/hooks/advisory/test_refactor_guard_advisory.py +0 -242
  80. package/hooks/advisory/test_refactor_guard_eligibility.py +0 -166
  81. package/hooks/hooks_constants/refactor_guard_constants.py +0 -77
  82. package/hooks/hooks_constants/test_refactor_guard_constants.py +0 -21
@@ -19,15 +19,19 @@ from dev_env_scripts_constants.claude_account_worker_constants import (
19
19
  DEFAULT_TIMEOUT_MINUTES,
20
20
  INVALID_TIMEOUT_MESSAGE,
21
21
  LAUNCH_FAILURE_EXIT_CODE,
22
+ LIVE_LOG_FLAG,
22
23
  MINIMUM_TIMEOUT_MINUTES,
23
24
  MODEL_FLAG,
24
25
  OUTPUT_FORMAT_FLAG,
25
26
  OUTPUT_FORMAT_JSON,
27
+ OUTPUT_FORMAT_STREAM_JSON,
26
28
  PERMISSION_MODE_FLAG,
27
29
  PROMPT_FILE_FLAG,
28
30
  REPORT_FILE_FLAG,
29
31
  SINGLE_PROMPT_FLAG,
30
32
  TIMEOUT_MINUTES_FLAG,
33
+ UNWRITABLE_LIVE_LOG_MESSAGE,
34
+ VERBOSE_FLAG,
31
35
  UTF8_ENCODING,
32
36
  )
33
37
 
@@ -50,15 +54,16 @@ def build_argument_parser() -> argparse.ArgumentParser:
50
54
  parser.add_argument(MODEL_FLAG)
51
55
  parser.add_argument(PERMISSION_MODE_FLAG, default=DEFAULT_PERMISSION_MODE)
52
56
  parser.add_argument(TIMEOUT_MINUTES_FLAG, type=_positive_timeout_minutes, default=DEFAULT_TIMEOUT_MINUTES)
57
+ parser.add_argument(LIVE_LOG_FLAG, type=Path)
53
58
  return parser
54
59
 
55
60
 
56
- def _invocation(*, model: str | None, permission_mode: str) -> list[str]:
61
+ def _invocation(*, model: str | None, permission_mode: str, is_streamed: bool) -> list[str]:
62
+ format_flags = [OUTPUT_FORMAT_FLAG, OUTPUT_FORMAT_STREAM_JSON, VERBOSE_FLAG] if is_streamed else [OUTPUT_FORMAT_FLAG, OUTPUT_FORMAT_JSON]
57
63
  arguments = [
58
64
  CLAUDE_BINARY_NAME,
59
65
  SINGLE_PROMPT_FLAG,
60
- OUTPUT_FORMAT_FLAG,
61
- OUTPUT_FORMAT_JSON,
66
+ *format_flags,
62
67
  PERMISSION_MODE_FLAG,
63
68
  permission_mode,
64
69
  ]
@@ -81,20 +86,33 @@ def run_worker(
81
86
  model: str | None,
82
87
  permission_mode: str,
83
88
  timeout_minutes: int,
89
+ live_log: Path | None = None,
84
90
  ) -> int:
85
- """Run one worker and write its structured report."""
91
+ """Run one worker and write its structured report.
92
+
93
+ With ``live_log`` set, the worker streams one JSON event per line into that
94
+ file while it runs, so a watching session can read progress before the end.
95
+ A live log that cannot be written ends the run before the broker starts.
96
+ """
86
97
  try:
87
98
  prompt_text = prompt_file.read_text(encoding=UTF8_ENCODING)
88
99
  except (OSError, UnicodeError):
89
100
  report = pre_launch_failure_report("none", "prompt file is unreadable", LAUNCH_FAILURE_EXIT_CODE)
90
101
  return finalize_report(report_file, report)
102
+ if live_log is not None:
103
+ try:
104
+ live_log.write_bytes(b"")
105
+ except OSError:
106
+ report = pre_launch_failure_report("none", UNWRITABLE_LIVE_LOG_MESSAGE, LAUNCH_FAILURE_EXIT_CODE)
107
+ return finalize_report(report_file, report)
91
108
  try:
92
109
  outcome, duration_seconds = invoke_worker(
93
- all_arguments=_invocation(model=model, permission_mode=permission_mode),
110
+ all_arguments=_invocation(model=model, permission_mode=permission_mode, is_streamed=live_log is not None),
94
111
  cwd=cwd,
95
112
  prompt_text=prompt_text,
96
113
  timeout_minutes=timeout_minutes,
97
114
  runner=worker_job_runner,
115
+ live_log=live_log,
98
116
  )
99
117
  except BrokerConfigurationError as error:
100
118
  report = pre_launch_failure_report("none", str(error), CONFIGURATION_FAILURE_EXIT_CODE)
@@ -122,6 +140,7 @@ def main() -> int:
122
140
  model=arguments.model,
123
141
  permission_mode=arguments.permission_mode,
124
142
  timeout_minutes=arguments.timeout_minutes,
143
+ live_log=arguments.live_log,
125
144
  )
126
145
 
127
146
 
@@ -17,6 +17,7 @@ def invoke_worker(
17
17
  prompt_text: str,
18
18
  timeout_minutes: int,
19
19
  runner: Callable[..., JobOutcome],
20
+ live_log: Path | None = None,
20
21
  monotonic_clock: Callable[[], float] = time.monotonic,
21
22
  ) -> tuple[JobOutcome, float]:
22
23
  """Run one broker job and measure its elapsed time.
@@ -28,12 +29,15 @@ def invoke_worker(
28
29
  timeout_minutes: Job time limit in minutes; the runner receives it in seconds.
29
30
  runner: Broker job runner, called with ``Product.CLAUDE``,
30
31
  ``encoding="utf-8"``, and ``errors="replace"``.
32
+ live_log: File the runner writes the job's standard output into as it
33
+ runs, passed only when set.
31
34
  monotonic_clock: Clock read once before and once after the job.
32
35
 
33
36
  Returns:
34
37
  The runner's job outcome and the difference between the two clock reads.
35
38
  """
36
39
  started_at = monotonic_clock()
40
+ all_log_options = {} if live_log is None else {"live_log": live_log}
37
41
  outcome = runner(
38
42
  Product.CLAUDE,
39
43
  all_arguments,
@@ -42,5 +46,6 @@ def invoke_worker(
42
46
  cwd=cwd,
43
47
  encoding="utf-8",
44
48
  errors="replace",
49
+ **all_log_options,
45
50
  )
46
51
  return outcome, monotonic_clock() - started_at
@@ -9,6 +9,7 @@ from pathlib import Path
9
9
 
10
10
  from dev_env_scripts_constants.claude_account_worker_constants import (
11
11
  DURATION_DECIMAL_PLACES,
12
+ JSON_EVENT_TYPE_KEY,
12
13
  JSON_IS_ERROR_KEY,
13
14
  JSON_RESULT_KEY,
14
15
  REPORT_ACCOUNT_KEY,
@@ -18,6 +19,7 @@ from dev_env_scripts_constants.claude_account_worker_constants import (
18
19
  REPORT_IS_ERROR_KEY,
19
20
  REPORT_REASON_KEY,
20
21
  REPORT_RESULT_KEY,
22
+ RESULT_EVENT_TYPE,
21
23
  STDOUT_TAIL_CHARACTER_LIMIT,
22
24
  SUMMARY_LINE_TEMPLATE,
23
25
  UTF8_ENCODING,
@@ -41,11 +43,24 @@ def _bounded_stdout_tail(stdout_text: str) -> str:
41
43
  return stdout_text[-STDOUT_TAIL_CHARACTER_LIMIT:]
42
44
 
43
45
 
46
+ def _closing_event(stdout_text: str) -> dict[str, object] | None:
47
+ for each_line in reversed(stdout_text.splitlines()):
48
+ try:
49
+ parsed_line = json.loads(each_line)
50
+ except json.JSONDecodeError:
51
+ continue
52
+ if isinstance(parsed_line, dict) and parsed_line.get(JSON_EVENT_TYPE_KEY) == RESULT_EVENT_TYPE:
53
+ return parsed_line
54
+ return None
55
+
56
+
44
57
  def _extract_payload(stdout_text: str) -> tuple[object, bool]:
45
58
  try:
46
59
  parsed_stdout = json.loads(stdout_text)
47
60
  except json.JSONDecodeError:
48
- return _bounded_stdout_tail(stdout_text), True
61
+ parsed_stdout = _closing_event(stdout_text)
62
+ if parsed_stdout is None:
63
+ return _bounded_stdout_tail(stdout_text), True
49
64
  if not isinstance(parsed_stdout, dict) or JSON_RESULT_KEY not in parsed_stdout:
50
65
  return None, True
51
66
  return parsed_stdout[JSON_RESULT_KEY], parsed_stdout.get(JSON_IS_ERROR_KEY) is True
@@ -43,6 +43,16 @@ OUTPUT_FORMAT_FLAG: str = "--output-format"
43
43
 
44
44
  OUTPUT_FORMAT_JSON: str = "json"
45
45
 
46
+ OUTPUT_FORMAT_STREAM_JSON: str = "stream-json"
47
+
48
+ VERBOSE_FLAG: str = "--verbose"
49
+
50
+ LIVE_LOG_FLAG: str = "--live-log"
51
+
52
+ JSON_EVENT_TYPE_KEY: str = "type"
53
+
54
+ RESULT_EVENT_TYPE: str = "result"
55
+
46
56
  SINGLE_PROMPT_FLAG: str = "-p"
47
57
 
48
58
  SECONDS_PER_MINUTE: int = 60
@@ -67,6 +77,8 @@ SUMMARY_LINE_TEMPLATE: str = "account={account} exit_code={exit_code} report={re
67
77
 
68
78
  INVALID_TIMEOUT_MESSAGE: str = "--timeout-minutes must be at least 1"
69
79
 
80
+ UNWRITABLE_LIVE_LOG_MESSAGE: str = "live log file is unwritable"
81
+
70
82
 
71
83
  __all__ = [
72
84
  "CLAUDE_BINARY_NAME",
@@ -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
@@ -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