claude-dev-env 8.6.1 → 8.8.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 (119) hide show
  1. package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  2. package/.agents/skills/team-advisor/SKILL.md +2 -2
  3. package/_shared/advisor/reference/identity.md +1 -1
  4. package/_shared/advisor/reference/spawn-walk-log.md +1 -1
  5. package/_shared/advisor/reference/warm-up.md +3 -1
  6. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +16 -0
  7. package/_shared/advisor/scripts/model_tier_run_validator.py +23 -5
  8. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +85 -6
  9. package/_shared/pr-loop/scripts/terminology_sweep.py +10 -9
  10. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -0
  11. package/bin/install.test.mjs +25 -0
  12. package/docs/high-trust-agent-delivery.md +132 -0
  13. package/docs/references/advisor-tool.md +3 -2
  14. package/docs/second-claude-account.md +50 -0
  15. package/hooks/advisory/refactor_guard.py +2 -2
  16. package/hooks/advisory/test_refactor_guard.py +21 -0
  17. package/hooks/blocking/code_rules_enforcer_test_support.py +10 -0
  18. package/hooks/blocking/code_rules_shared.py +4 -7
  19. package/hooks/blocking/conftest.py +22 -0
  20. package/hooks/blocking/state_description_blocker.py +10 -8
  21. package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +2 -11
  22. package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +4 -29
  23. package/hooks/blocking/test_code_rules_enforcer_test_support.py +21 -0
  24. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +3 -19
  25. package/hooks/blocking/test_code_rules_shared.py +18 -0
  26. package/hooks/blocking/test_conftest.py +35 -0
  27. package/hooks/blocking/test_pii_scanner.py +8 -0
  28. package/hooks/blocking/test_state_description_blocker.py +16 -0
  29. package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -14
  30. package/hooks/git-hooks/pull_request_handoff.py +2 -2
  31. package/hooks/git-hooks/test_config.py +10 -3
  32. package/hooks/git-hooks/test_pull_request_handoff.py +49 -0
  33. package/hooks/git-hooks/test_verification_notice_state.py +50 -0
  34. package/hooks/git-hooks/verification_notice_state.py +3 -1
  35. package/hooks/hooks_constants/code_rules_enforcer_constants.py +13 -5
  36. package/hooks/hooks_constants/pii_prevention_constants.py +1 -0
  37. package/hooks/hooks_constants/refactor_guard_constants.py +2 -0
  38. package/hooks/hooks_constants/session_edit_stage_gate_constants.py +3 -4
  39. package/hooks/hooks_constants/state_description_blocker_constants.py +0 -3
  40. package/hooks/hooks_constants/test_state_description_blocker_constants.py +9 -7
  41. package/hooks/validators/exempt_paths.py +3 -18
  42. package/hooks/validators/test_exempt_paths.py +20 -0
  43. package/hooks/validators/test_python_antipattern_checks.py +14 -0
  44. package/package.json +1 -1
  45. package/rules/agent-merges-its-own-green-pull-request.md +54 -0
  46. package/rules/archiving-agent-config.md +10 -0
  47. package/rules/code-standards.md +1 -0
  48. package/rules/correction-lens.md +134 -0
  49. package/rules/flag-non-breaking-findings.md +11 -0
  50. package/rules/long-horizon-autonomy.md +11 -0
  51. package/rules/no-contrast-framing.md +68 -0
  52. package/rules/review-closure-is-a-check.md +44 -0
  53. package/scripts/Invoke-PesterSuites.ps1 +42 -0
  54. package/scripts/agent_merge_check.py +381 -0
  55. package/scripts/check.ps1 +12 -1
  56. package/scripts/ci/install-playtest.mjs +291 -0
  57. package/scripts/ci/install-playtest.test.mjs +84 -0
  58. package/scripts/ci/installer-lifecycle.mjs +11 -30
  59. package/scripts/ci/installer-lifecycle.test.mjs +14 -0
  60. package/scripts/ci/pstack-command-shim.test.mjs +33 -0
  61. package/scripts/ci/scratch-home-install.mjs +73 -0
  62. package/scripts/ci/scratch-home-install.test.mjs +49 -0
  63. package/scripts/claude_account_choice.py +246 -0
  64. package/scripts/claude_account_profile.py +371 -0
  65. package/scripts/claude_chain_usage.py +49 -5
  66. package/scripts/codex_compat_materializer.py +33 -16
  67. package/scripts/contrast_framing.py +101 -0
  68. package/scripts/dev_env_scripts_constants/agent_merge_check_constants.py +121 -0
  69. package/scripts/dev_env_scripts_constants/claude_account_constants.py +166 -0
  70. package/scripts/dev_env_scripts_constants/contrast_framing_constants.py +82 -0
  71. package/scripts/dev_env_scripts_constants/merge_gate_check_constants.py +23 -0
  72. package/scripts/dev_env_scripts_constants/review_closure_constants.py +241 -0
  73. package/scripts/dev_env_scripts_constants/test_claude_account_constants.py +21 -0
  74. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +12 -0
  75. package/scripts/dev_env_scripts_constants/test_contrast_framing_constants.py +60 -0
  76. package/scripts/durable_post_lint.py +30 -4
  77. package/scripts/followup_cli.py +6 -10
  78. package/scripts/merge_gate_checks.py +236 -0
  79. package/scripts/policy_lint/adapter_contrast_framing.py +59 -0
  80. package/scripts/policy_lint/adapters.py +30 -0
  81. package/scripts/policy_lint/config/constants.py +2 -0
  82. package/scripts/policy_lint/registry.py +5 -0
  83. package/scripts/repository_checks/config/constants.py +12 -0
  84. package/scripts/repository_checks/config/policy_document.py +28 -2
  85. package/scripts/repository_checks/followups.py +54 -0
  86. package/scripts/repository_checks/match_exemptions.py +67 -0
  87. package/scripts/repository_checks/models.py +46 -2
  88. package/scripts/repository_checks/reporting.py +23 -7
  89. package/scripts/repository_checks/tracked_secrets.py +2 -2
  90. package/scripts/repository_policy.py +4 -2
  91. package/scripts/review_closure.py +134 -0
  92. package/scripts/review_closure_github.py +345 -0
  93. package/scripts/review_closure_model.py +360 -0
  94. package/scripts/test_agent_merge_check.py +317 -0
  95. package/scripts/test_claude_account_choice.py +217 -0
  96. package/scripts/test_claude_account_profile.py +220 -0
  97. package/scripts/test_claude_chain_usage.py +40 -0
  98. package/scripts/test_durable_post_lint.py +46 -0
  99. package/scripts/test_followup_cli.py +40 -0
  100. package/scripts/test_merge_gate_checks.py +92 -0
  101. package/scripts/tests/test_code_review_constants.py +78 -0
  102. package/scripts/tests/test_codex_compat_materializer.py +48 -4
  103. package/scripts/tests/test_contrast_framing.py +117 -0
  104. package/scripts/tests/test_followups.py +88 -0
  105. package/scripts/tests/test_match_exemptions.py +291 -0
  106. package/scripts/tests/test_models.py +67 -2
  107. package/scripts/tests/test_package_inventory.py +6 -4
  108. package/scripts/tests/test_pester_suites_run.py +53 -0
  109. package/scripts/tests/test_policy_document.py +39 -1
  110. package/scripts/tests/test_policy_lint_rules_contrast_framing.py +68 -0
  111. package/scripts/tests/test_reporting.py +18 -0
  112. package/scripts/tests/test_repository_policy.py +43 -1
  113. package/scripts/tests/test_review_closure.py +126 -0
  114. package/scripts/tests/test_review_closure_github.py +240 -0
  115. package/scripts/tests/test_review_closure_model.py +299 -0
  116. package/scripts/tests/test_tracked_secrets.py +42 -0
  117. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +0 -12
  118. package/scripts/repository_checks/email_exemptions.py +0 -39
  119. package/scripts/tests/test_email_exemptions.py +0 -158
@@ -0,0 +1,220 @@
1
+ """Tests for the second-account profile sync and its launcher."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ import claude_account_profile as profile
13
+
14
+ NOW = datetime(2026, 9, 22, 21, 0, tzinfo=timezone.utc)
15
+
16
+
17
+ def build_main_home(tmp_path: Path) -> Path:
18
+ main_home = tmp_path / "main"
19
+ (main_home / "skills" / "tdd").mkdir(parents=True)
20
+ (main_home / "rules").mkdir()
21
+ (main_home / "projects").mkdir()
22
+ (main_home / "CLAUDE.md").write_text("main instructions", encoding="utf-8")
23
+ (main_home / "settings.json").write_text("{}", encoding="utf-8")
24
+ (main_home / ".credentials.json").write_text("main sign-in", encoding="utf-8")
25
+ (main_home / ".claude.json").write_text("main state", encoding="utf-8")
26
+ (main_home / ".claude.json.backup").write_text("main backup", encoding="utf-8")
27
+ return main_home
28
+
29
+
30
+ def sync(main_home: Path, profile_home: Path) -> profile.ProfileSyncReport:
31
+ return profile.sync_profile(main_home=main_home, profile_home=profile_home, now=NOW)
32
+
33
+
34
+ class TestDefaultProfileHome:
35
+ def should_use_the_profiles_root_the_environment_names(
36
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
37
+ ) -> None:
38
+ monkeypatch.setenv("LLM_SETTINGS_PROFILES_ROOT", str(tmp_path))
39
+ assert profile.default_profile_home() == tmp_path / "ev"
40
+
41
+ def should_fall_back_to_the_profiles_root_in_the_user_home(
42
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
43
+ ) -> None:
44
+ monkeypatch.delenv("LLM_SETTINGS_PROFILES_ROOT", raising=False)
45
+ monkeypatch.setattr(Path, "home", lambda: tmp_path)
46
+ assert profile.default_profile_home() == tmp_path / ".claude-profiles" / "ev"
47
+
48
+
49
+ class TestIsAccountLocal:
50
+ def should_keep_sign_in_state_and_history_local(self) -> None:
51
+ for each_name in (
52
+ ".credentials.json",
53
+ ".credentials.json.bak-session-dashboard",
54
+ ".claude.json",
55
+ ".claude.json.backup",
56
+ "projects",
57
+ ):
58
+ assert profile.is_account_local(each_name)
59
+
60
+ def should_share_skills_rules_and_settings(self) -> None:
61
+ for each_name in ("skills", "rules", "CLAUDE.md", "settings.json", "plugins"):
62
+ assert not profile.is_account_local(each_name)
63
+
64
+
65
+ class TestSyncProfile:
66
+ def should_link_every_shared_entry_to_the_main_home(self, tmp_path: Path) -> None:
67
+ main_home = build_main_home(tmp_path)
68
+ profile_home = tmp_path / "ev"
69
+ sync(main_home, profile_home)
70
+ assert (profile_home / "skills" / "tdd").is_dir()
71
+ assert (profile_home / "CLAUDE.md").read_text(
72
+ encoding="utf-8"
73
+ ) == "main instructions"
74
+ assert profile.links_to(profile_home / "rules", main_home / "rules")
75
+ assert profile.links_to(
76
+ profile_home / "settings.json", main_home / "settings.json"
77
+ )
78
+
79
+ def should_keep_the_accounts_own_sign_in_state_and_history_apart(
80
+ self, tmp_path: Path
81
+ ) -> None:
82
+ main_home = build_main_home(tmp_path)
83
+ profile_home = tmp_path / "ev"
84
+ profile_home.mkdir()
85
+ (profile_home / ".credentials.json").write_text(
86
+ "second sign-in", encoding="utf-8"
87
+ )
88
+ sync(main_home, profile_home)
89
+ assert (profile_home / ".credentials.json").read_text(
90
+ encoding="utf-8"
91
+ ) == "second sign-in"
92
+ assert not (profile_home / ".claude.json").exists()
93
+ assert not (profile_home / ".claude.json.backup").exists()
94
+ assert not (profile_home / "projects").exists()
95
+
96
+ def should_move_a_stale_copy_aside_and_keep_its_content(
97
+ self, tmp_path: Path
98
+ ) -> None:
99
+ main_home = build_main_home(tmp_path)
100
+ profile_home = tmp_path / "ev"
101
+ profile_home.mkdir()
102
+ (profile_home / "CLAUDE.md").write_text("stale copy", encoding="utf-8")
103
+ report = sync(main_home, profile_home)
104
+ moved_copy = (
105
+ profile_home / ".replaced" / NOW.strftime("%Y%m%dT%H%M%SZ") / "CLAUDE.md"
106
+ )
107
+ assert moved_copy.read_text(encoding="utf-8") == "stale copy"
108
+ assert profile.links_to(profile_home / "CLAUDE.md", main_home / "CLAUDE.md")
109
+ assert report.all_moved_aside == ("CLAUDE.md",)
110
+
111
+ def should_remove_a_same_content_copy_without_moving_it_aside(
112
+ self, tmp_path: Path
113
+ ) -> None:
114
+ main_home = build_main_home(tmp_path)
115
+ profile_home = tmp_path / "ev"
116
+ profile_home.mkdir()
117
+ (profile_home / "CLAUDE.md").write_text("main instructions", encoding="utf-8")
118
+ report = sync(main_home, profile_home)
119
+ assert report.all_moved_aside == ()
120
+ assert not (profile_home / ".replaced").exists()
121
+ assert profile.links_to(profile_home / "CLAUDE.md", main_home / "CLAUDE.md")
122
+
123
+ def should_move_aside_a_link_that_points_somewhere_else(
124
+ self, tmp_path: Path
125
+ ) -> None:
126
+ main_home = build_main_home(tmp_path)
127
+ elsewhere = tmp_path / "old-shared" / "rules"
128
+ elsewhere.mkdir(parents=True)
129
+ (elsewhere / "keep.md").write_text("old rule", encoding="utf-8")
130
+ profile_home = tmp_path / "ev"
131
+ profile_home.mkdir()
132
+ (profile_home / "rules").symlink_to(elsewhere, target_is_directory=True)
133
+ report = sync(main_home, profile_home)
134
+ assert (elsewhere / "keep.md").read_text(encoding="utf-8") == "old rule"
135
+ assert profile.links_to(profile_home / "rules", main_home / "rules")
136
+ assert report.all_moved_aside == ("rules",)
137
+
138
+ def should_change_nothing_on_a_second_run(self, tmp_path: Path) -> None:
139
+ main_home = build_main_home(tmp_path)
140
+ profile_home = tmp_path / "ev"
141
+ sync(main_home, profile_home)
142
+ second_report = sync(main_home, profile_home)
143
+ assert second_report.all_linked == ()
144
+ assert second_report.all_moved_aside == ()
145
+ assert second_report.all_unlinked == ()
146
+
147
+ def should_unlink_an_entry_the_main_home_no_longer_has(
148
+ self, tmp_path: Path
149
+ ) -> None:
150
+ main_home = build_main_home(tmp_path)
151
+ profile_home = tmp_path / "ev"
152
+ sync(main_home, profile_home)
153
+ (main_home / "rules").rmdir()
154
+ report = sync(main_home, profile_home)
155
+ assert not os.path.lexists(profile_home / "rules")
156
+ assert report.all_unlinked == ("rules",)
157
+
158
+
159
+ def should_remove_a_main_link_left_under_an_account_local_name(
160
+ self, tmp_path: Path
161
+ ) -> None:
162
+ main_home = build_main_home(tmp_path)
163
+ (main_home / ".credentials.json.bak").write_text("main backup", encoding="utf-8")
164
+ profile_home = tmp_path / "ev"
165
+ profile_home.mkdir()
166
+ os.link(main_home / ".credentials.json.bak", profile_home / ".credentials.json.bak")
167
+ report = sync(main_home, profile_home)
168
+ assert not os.path.lexists(profile_home / ".credentials.json.bak")
169
+ assert (main_home / ".credentials.json.bak").read_text(
170
+ encoding="utf-8"
171
+ ) == "main backup"
172
+ assert report.all_unlinked == (".credentials.json.bak",)
173
+
174
+
175
+ class TestWriteLauncher:
176
+ def should_point_claude_at_the_profile_and_pass_every_argument(
177
+ self, tmp_path: Path
178
+ ) -> None:
179
+ profile_home = tmp_path / "ev"
180
+ launcher_path = profile.write_launcher(
181
+ launcher_directory=tmp_path / "bin", profile_home=profile_home, now=NOW
182
+ )
183
+ launcher_text = launcher_path.read_text(encoding="utf-8")
184
+ assert f'set "CLAUDE_CONFIG_DIR={profile_home}"' in launcher_text
185
+ assert "claude %*" in launcher_text
186
+
187
+ def should_move_an_older_launcher_aside(self, tmp_path: Path) -> None:
188
+ launcher_directory = tmp_path / "bin"
189
+ launcher_directory.mkdir()
190
+ (launcher_directory / "claude-ev.cmd").write_text(
191
+ "old launcher", encoding="utf-8"
192
+ )
193
+ profile.write_launcher(
194
+ launcher_directory=launcher_directory, profile_home=tmp_path / "ev", now=NOW
195
+ )
196
+ moved_launcher = launcher_directory / (
197
+ "claude-ev.cmd.replaced-" + NOW.strftime("%Y%m%dT%H%M%SZ")
198
+ )
199
+ assert moved_launcher.read_text(encoding="utf-8") == "old launcher"
200
+
201
+
202
+ class TestMain:
203
+ def should_print_what_the_sync_changed(
204
+ self, tmp_path: Path, capsys: pytest.CaptureFixture[str]
205
+ ) -> None:
206
+ main_home = build_main_home(tmp_path)
207
+ exit_code = profile.main(
208
+ [
209
+ "--main-home",
210
+ str(main_home),
211
+ "--profile-home",
212
+ str(tmp_path / "ev"),
213
+ "--launcher-directory",
214
+ str(tmp_path / "bin"),
215
+ ]
216
+ )
217
+ printed = json.loads(capsys.readouterr().out)
218
+ assert exit_code == 0
219
+ assert "CLAUDE.md" in printed["linked"]
220
+ assert printed["launcher"] == str(tmp_path / "bin" / "claude-ev.cmd")
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import json
6
6
  import subprocess
7
7
  import sys
8
+ from datetime import datetime
8
9
  from pathlib import Path
9
10
 
10
11
  import pytest
@@ -372,7 +373,10 @@ def test_probe_weekly_utilization_reuses_resolver_windows(
372
373
  monkeypatch: pytest.MonkeyPatch,
373
374
  ) -> None:
374
375
  class _FakeWindows:
376
+ session_utilization = None
377
+ session_resets_at = None
375
378
  weekly_utilization = 37.5
379
+ weekly_resets_at = None
376
380
 
377
381
  class _FakeResolver:
378
382
  def read_oauth_access_token(self, credentials_path: Path, now: object) -> str:
@@ -401,6 +405,42 @@ def test_probe_weekly_utilization_reuses_resolver_windows(
401
405
  assert weekly_utilization == pytest.approx(37.5)
402
406
 
403
407
 
408
+ def should_read_both_meters_and_their_resets_for_one_account(
409
+ monkeypatch: pytest.MonkeyPatch,
410
+ ) -> None:
411
+ session_reset = datetime(2026, 9, 22, 23, 0).astimezone()
412
+ weekly_reset = datetime(2026, 9, 26, 9, 0).astimezone()
413
+
414
+ class _FakeWindows:
415
+ session_utilization = 12.0
416
+ session_resets_at = session_reset
417
+ weekly_utilization = 64.0
418
+ weekly_resets_at = weekly_reset
419
+
420
+ class _FakeResolver:
421
+ def read_oauth_access_token(self, credentials_path: Path, now: object) -> str:
422
+ return "token-value"
423
+
424
+ def _fetch_usage_payload(self, access_token: str) -> dict[str, object]:
425
+ return {}
426
+
427
+ def extract_usage_windows(
428
+ self, usage_payload: dict[str, object]
429
+ ) -> _FakeWindows:
430
+ return _FakeWindows()
431
+
432
+ monkeypatch.setattr(
433
+ usage, "_load_resolve_usage_window_module", lambda: _FakeResolver()
434
+ )
435
+ account_meters = usage.probe_account_meters(Path(PLACEHOLDER_CREDENTIALS_PRIMARY))
436
+ assert account_meters == usage.AccountUsageMeters(
437
+ session_utilization=12.0,
438
+ session_resets_at=session_reset,
439
+ weekly_utilization=64.0,
440
+ weekly_resets_at=weekly_reset,
441
+ )
442
+
443
+
404
444
  def test_probe_weekly_utilization_raises_when_token_missing(
405
445
  monkeypatch: pytest.MonkeyPatch,
406
446
  ) -> None:
@@ -460,3 +460,49 @@ def should_report_a_rewritten_release_body_through_the_command_line(
460
460
  )
461
461
  assert completed_process.returncode == 1
462
462
  assert "keep the generated text whole" in completed_process.stderr
463
+
464
+
465
+ def test_contrast_framing_in_a_body_reports() -> None:
466
+ all_findings = durable_post_lint.lint_durable_post(
467
+ action="pr-comment",
468
+ title=None,
469
+ body_text="The queue ejected the entry, not the runner.\n",
470
+ )
471
+
472
+ assert [each_finding.code for each_finding in all_findings] == ["contrast-framing"]
473
+ assert "trailing-comma-not" in all_findings[0].message
474
+
475
+
476
+ def test_contrast_framing_in_a_title_reports() -> None:
477
+ all_findings = durable_post_lint.lint_durable_post(
478
+ action="pr-create",
479
+ title="fix(lint): read the base rather than the worktree",
480
+ body_text=VALID_PR_BODY,
481
+ )
482
+
483
+ assert [each_finding.code for each_finding in all_findings] == ["contrast-framing"]
484
+ assert "substitution-rather-than" in all_findings[0].message
485
+
486
+
487
+ def test_a_body_stating_what_is_stays_clean() -> None:
488
+ all_findings = durable_post_lint.lint_durable_post(
489
+ action="pr-comment",
490
+ title=None,
491
+ body_text="The queue ejected the entry. The runner stayed up.\n",
492
+ )
493
+
494
+ assert all_findings == ()
495
+
496
+
497
+ def test_a_release_body_carrying_a_contrast_stays_accepted() -> None:
498
+ all_findings = durable_post_lint.lint_durable_post(
499
+ action="pr-edit",
500
+ title=None,
501
+ body_text=RELEASE_BODY.replace(
502
+ "This PR was generated with",
503
+ "Bumped rather than pinned. This PR was generated with",
504
+ ),
505
+ head_branch=RELEASE_BRANCH,
506
+ )
507
+
508
+ assert all_findings == ()
@@ -9,6 +9,8 @@ from pathlib import Path
9
9
 
10
10
  from dev_env_scripts_constants.followup_constants import (
11
11
  BACKLOG_EXCEEDED_EXIT_CODE,
12
+ BACKLOG_EXCEEDED_TEMPLATE,
13
+ BACKLOG_WITHIN_TEMPLATE,
12
14
  FOLLOWUP_BACKLOG_THRESHOLD,
13
15
  )
14
16
  from followup_cli import main
@@ -251,6 +253,44 @@ def test_count_escalates_once_the_backlog_passes_the_threshold(
251
253
  assert str(FOLLOWUP_BACKLOG_THRESHOLD) in output_text
252
254
 
253
255
 
256
+ def test_count_writes_the_within_threshold_template_verbatim(
257
+ tmp_path: Path,
258
+ ) -> None:
259
+ exit_code, output_text = run_command(["count", "--repository-root", str(tmp_path)])
260
+
261
+ assert exit_code == 0
262
+ assert output_text == (
263
+ BACKLOG_WITHIN_TEMPLATE.format(
264
+ finding_count=0, threshold=FOLLOWUP_BACKLOG_THRESHOLD
265
+ )
266
+ + "\n"
267
+ )
268
+
269
+
270
+ def test_count_writes_the_exceeded_template_verbatim(tmp_path: Path) -> None:
271
+ for each_index in range(FOLLOWUP_BACKLOG_THRESHOLD + 1):
272
+ record_followup_finding(
273
+ tmp_path,
274
+ FollowupFinding(
275
+ "code-rules",
276
+ f"run{each_index}.py",
277
+ "one smell",
278
+ f"code-rules/x{each_index}",
279
+ ),
280
+ )
281
+
282
+ exit_code, output_text = run_command(["count", "--repository-root", str(tmp_path)])
283
+
284
+ assert exit_code == BACKLOG_EXCEEDED_EXIT_CODE
285
+ assert output_text == (
286
+ BACKLOG_EXCEEDED_TEMPLATE.format(
287
+ finding_count=FOLLOWUP_BACKLOG_THRESHOLD + 1,
288
+ threshold=FOLLOWUP_BACKLOG_THRESHOLD,
289
+ )
290
+ + "\n"
291
+ )
292
+
293
+
254
294
  def test_list_names_the_check_behind_each_finding(tmp_path: Path) -> None:
255
295
  record_followup_finding(
256
296
  tmp_path,
@@ -0,0 +1,92 @@
1
+ """Specifications for holding a merge policy document to its branch ruleset."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from merge_gate_checks import (
9
+ main,
10
+ missing_contexts,
11
+ parse_declared_contexts,
12
+ ruleset_contexts,
13
+ )
14
+
15
+ REPOSITORY_ROOT = Path(__file__).resolve().parents[3]
16
+ POLICY_DOCUMENT_PATH = REPOSITORY_ROOT / "docs" / "merge-policy.md"
17
+ RULESET_SNAPSHOT_PATH = REPOSITORY_ROOT / "docs" / "merge-policy-ruleset.json"
18
+ INSTRUCTION_PAIRS_CONTEXT = "instruction-pairs / instruction-pairs"
19
+ UTF8_ENCODING = "utf-8"
20
+
21
+
22
+ def _ruleset_with_contexts(*all_contexts: str) -> list[dict[str, object]]:
23
+ return [
24
+ {"type": "deletion"},
25
+ {
26
+ "type": "required_status_checks",
27
+ "parameters": {
28
+ "required_status_checks": [
29
+ {"context": each_context, "integration_id": 15368}
30
+ for each_context in all_contexts
31
+ ]
32
+ },
33
+ },
34
+ ]
35
+
36
+
37
+ def should_read_the_contexts_the_fenced_block_declares() -> None:
38
+ document_text = (
39
+ "# Merge policy\n\n"
40
+ "```required-status-checks\n"
41
+ f"{INSTRUCTION_PAIRS_CONTEXT}\n"
42
+ "```\n\n"
43
+ "```\n"
44
+ "python merge_gate_checks.py --refresh\n"
45
+ "```\n"
46
+ )
47
+
48
+ assert parse_declared_contexts(document_text) == [INSTRUCTION_PAIRS_CONTEXT]
49
+
50
+
51
+ def should_ignore_a_document_with_no_declaration_block() -> None:
52
+ assert parse_declared_contexts("# Merge policy\n\nNo block here.\n") == []
53
+
54
+
55
+ def should_read_every_context_the_ruleset_requires() -> None:
56
+ payload = _ruleset_with_contexts(INSTRUCTION_PAIRS_CONTEXT, "Ruff")
57
+
58
+ assert ruleset_contexts(payload) == {INSTRUCTION_PAIRS_CONTEXT, "Ruff"}
59
+
60
+
61
+ def should_report_a_check_the_ruleset_stopped_requiring() -> None:
62
+ document_text = f"```required-status-checks\n{INSTRUCTION_PAIRS_CONTEXT}\n```\n"
63
+
64
+ assert missing_contexts(document_text, _ruleset_with_contexts("Ruff")) == [
65
+ INSTRUCTION_PAIRS_CONTEXT
66
+ ]
67
+
68
+
69
+ def should_find_every_check_the_checked_in_policy_names() -> None:
70
+ document_text = POLICY_DOCUMENT_PATH.read_text(encoding=UTF8_ENCODING)
71
+ ruleset_payload = json.loads(
72
+ RULESET_SNAPSHOT_PATH.read_text(encoding=UTF8_ENCODING)
73
+ )
74
+
75
+ assert parse_declared_contexts(document_text)
76
+ assert missing_contexts(document_text, ruleset_payload) == []
77
+
78
+
79
+ def should_exit_non_zero_when_a_named_check_left_the_ruleset(tmp_path: Path) -> None:
80
+ document_path = tmp_path / "merge-policy.md"
81
+ document_path.write_text(
82
+ f"```required-status-checks\n{INSTRUCTION_PAIRS_CONTEXT}\n```\n",
83
+ encoding=UTF8_ENCODING,
84
+ )
85
+ ruleset_path = tmp_path / "merge-policy-ruleset.json"
86
+ ruleset_path.write_text(
87
+ json.dumps(_ruleset_with_contexts("Ruff")), encoding=UTF8_ENCODING
88
+ )
89
+
90
+ exit_code = main(["--document", str(document_path), "--ruleset", str(ruleset_path)])
91
+
92
+ assert exit_code == 1
@@ -0,0 +1,78 @@
1
+ """Specifications for the permission modes the review binary is asked for.
2
+
3
+ The review binary accepts one permission mode for an ordinary caller and a
4
+ different one for root. Naming the wrong mode means the binary refuses the
5
+ call, no review runs, and no stamp is ever minted.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib
11
+ import os
12
+
13
+ import pytest
14
+ from dev_env_scripts_constants import code_review_constants
15
+
16
+ BYPASS_MODE_TOKEN: str = "bypassPermissions"
17
+ ACCEPT_EDITS_MODE_TOKEN: str = "acceptEdits"
18
+ ROOT_USER_ID: int = 0
19
+ UNPRIVILEGED_USER_ID: int = 1000
20
+
21
+
22
+ def _permission_mode_seen_by(
23
+ monkeypatch: pytest.MonkeyPatch, effective_user_id: int
24
+ ) -> str:
25
+ """Read the review permission mode a caller with this user id resolves."""
26
+ monkeypatch.setattr(os, "geteuid", lambda: effective_user_id)
27
+ reloaded_constants = importlib.reload(code_review_constants)
28
+ return str(reloaded_constants.REVIEW_PERMISSION_MODE)
29
+
30
+
31
+ def test_root_detection_agrees_with_this_process(
32
+ monkeypatch: pytest.MonkeyPatch,
33
+ ) -> None:
34
+ monkeypatch.setattr(os, "geteuid", lambda: ROOT_USER_ID)
35
+ reloaded_constants = importlib.reload(code_review_constants)
36
+
37
+ assert reloaded_constants.IS_ROOT_CALLER is True
38
+ assert reloaded_constants.ROOT_EFFECTIVE_USER_ID == ROOT_USER_ID
39
+
40
+
41
+ def test_bypass_mode_names_the_token_the_binary_reads() -> None:
42
+ assert code_review_constants.PERMISSION_MODE_BYPASS == BYPASS_MODE_TOKEN
43
+
44
+
45
+ def test_accept_edits_mode_names_the_token_the_binary_reads() -> None:
46
+ assert code_review_constants.PERMISSION_MODE_ACCEPT_EDITS == ACCEPT_EDITS_MODE_TOKEN
47
+
48
+
49
+ def test_a_root_caller_resolves_the_mode_the_binary_accepts(
50
+ monkeypatch: pytest.MonkeyPatch,
51
+ ) -> None:
52
+ assert _permission_mode_seen_by(monkeypatch, ROOT_USER_ID) == (
53
+ ACCEPT_EDITS_MODE_TOKEN
54
+ )
55
+
56
+
57
+ def test_an_ordinary_caller_resolves_the_bypass_mode(
58
+ monkeypatch: pytest.MonkeyPatch,
59
+ ) -> None:
60
+ assert _permission_mode_seen_by(monkeypatch, UNPRIVILEGED_USER_ID) == (
61
+ BYPASS_MODE_TOKEN
62
+ )
63
+
64
+
65
+ def test_a_platform_without_a_user_id_still_loads_the_constants(
66
+ monkeypatch: pytest.MonkeyPatch,
67
+ ) -> None:
68
+ """Windows exposes no ``os.geteuid``, and the module still imports.
69
+
70
+ Reading a missing ``os.geteuid`` raises, and every caller of the review
71
+ invoker dies at import. A platform that reports no user id is never the
72
+ root the review binary refuses, so it reads as an ordinary caller.
73
+ """
74
+ monkeypatch.delattr(os, "geteuid", raising=False)
75
+ reloaded_constants = importlib.reload(code_review_constants)
76
+
77
+ assert reloaded_constants.IS_ROOT_CALLER is False
78
+ assert reloaded_constants.REVIEW_PERMISSION_MODE == BYPASS_MODE_TOKEN
@@ -173,9 +173,9 @@ def test_public_legacy_call_forms_validate_collection_entries(tmp_path: Path) ->
173
173
  publish_plan(config, all_planned_files=[object()])
174
174
 
175
175
 
176
- def test_render_codex_failure_blast_radius_requires_the_excerpt_heading() -> None:
177
- with pytest.raises(MaterializerError, match="requires a Codex excerpt"):
178
- materializer.render_codex_failure_blast_radius("# No excerpt\n")
176
+ def test_render_codex_instruction_excerpt_requires_the_excerpt_heading() -> None:
177
+ with pytest.raises(MaterializerError, match="rules/correction-lens.md requires a Codex excerpt"):
178
+ materializer.render_codex_instruction_excerpt("# No excerpt\n", "rules/correction-lens.md")
179
179
 
180
180
 
181
181
  def test_validation_rejects_reparse_point_from_portable_attribute_seam(
@@ -823,13 +823,57 @@ def test_failure_blast_radius_projection_uses_the_canonical_excerpt() -> None:
823
823
  canonical_rule_path = Path(__file__).parents[2] / "rules" / "failure-blast-radius.md"
824
824
  canonical_rule = canonical_rule_path.read_text(encoding="utf-8")
825
825
 
826
- projected_instruction = materializer.render_codex_failure_blast_radius(canonical_rule)
826
+ projected_instruction = materializer.render_codex_instruction_excerpt(
827
+ canonical_rule, "rules/failure-blast-radius.md"
828
+ )
827
829
 
828
830
  assert projected_instruction.startswith("Failure handling for this run")
829
831
  assert "Three attempts, then park." in projected_instruction
830
832
  assert "Report as: N of M complete" in projected_instruction
831
833
 
832
834
 
835
+ def test_codex_instruction_projection_carries_every_listed_rule(tmp_path: Path) -> None:
836
+ rules_root = Path(__file__).parents[2] / "rules"
837
+ source = tmp_path / "source"
838
+ target = tmp_path / "target"
839
+ (source / "rules").mkdir(parents=True)
840
+ for each_relative_path in materializer.codex_instruction_rule_relative_paths:
841
+ canonical_path = rules_root / Path(each_relative_path).name
842
+ (source / each_relative_path).write_text(
843
+ canonical_path.read_text(encoding="utf-8"), encoding="utf-8"
844
+ )
845
+ config = MaterializerConfig(source, target, should_apply=True)
846
+
847
+ planned, report = build_plan(config, all_agents=[])
848
+ publish_plan(config, planned, report)
849
+
850
+ projected_instruction = (target / "AGENTS.md").read_text(encoding="utf-8")
851
+ assert "Failure handling for this run" in projected_instruction
852
+ assert "Correction handling for this run" in projected_instruction
853
+ manifest_record = _manifest_files(_required_manifest_path(config))["AGENTS.md"]
854
+ assert manifest_record["source"] == "rules/failure-blast-radius.md, rules/correction-lens.md"
855
+
856
+
857
+ def test_codex_instruction_projection_skips_an_absent_listed_rule(
858
+ tmp_path: Path,
859
+ ) -> None:
860
+ rules_root = Path(__file__).parents[2] / "rules"
861
+ source = tmp_path / "source"
862
+ (source / "rules").mkdir(parents=True)
863
+ (source / "rules" / "correction-lens.md").write_text(
864
+ (rules_root / "correction-lens.md").read_text(encoding="utf-8"),
865
+ encoding="utf-8",
866
+ )
867
+ config = MaterializerConfig(source, tmp_path / "target", should_apply=False)
868
+
869
+ projection = materializer._build_codex_instruction_projection(config)
870
+
871
+ assert projection is not None
872
+ assert projection.source_identity == "rules/correction-lens.md"
873
+ assert "Correction handling for this run" in projection.content
874
+ assert "Failure handling for this run" not in projection.content
875
+
876
+
833
877
  def test_build_plan_publishes_owned_agents_projection_and_tracks_drift(
834
878
  tmp_path: Path,
835
879
  ) -> None: