claude-dev-env 1.95.0 → 2.0.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 (241) hide show
  1. package/_shared/advisor/CLAUDE.md +2 -2
  2. package/_shared/advisor/advisor-protocol.md +20 -20
  3. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +15 -12
  4. package/_shared/advisor/scripts/model_tier_run_validator.py +11 -10
  5. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +25 -19
  6. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
  7. package/_shared/advisor/scripts/tier_model_ids.py +18 -18
  8. package/_shared/pr-loop/CLAUDE.md +1 -0
  9. package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
  10. package/_shared/pr-loop/scripts/README.md +1 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
  13. package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
  14. package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
  15. package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
  16. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
  17. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
  18. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
  19. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
  22. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
  23. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
  24. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
  25. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
  26. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
  27. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
  28. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
  29. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
  30. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
  31. package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
  32. package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
  33. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
  34. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
  35. package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
  36. package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
  37. package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
  38. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
  39. package/_shared/pr-loop/worker-spawn.md +186 -0
  40. package/agents/code-verifier.md +1 -1
  41. package/bin/ever-shipped-skills.mjs +3 -0
  42. package/bin/expand_home_directory_tokens.mjs +1 -1
  43. package/bin/install.mjs +5 -2
  44. package/hooks/advisory/refactor_guard.py +3 -4
  45. package/hooks/blocking/CLAUDE.md +7 -1
  46. package/hooks/blocking/block_main_commit.py +2 -2
  47. package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
  48. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
  49. package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
  50. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
  51. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
  52. package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
  53. package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
  54. package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
  55. package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
  56. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
  57. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
  58. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
  59. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
  60. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
  61. package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
  62. package/hooks/blocking/code_rules_mock_completeness.py +1 -1
  63. package/hooks/blocking/code_rules_optional_params.py +2 -2
  64. package/hooks/blocking/code_rules_shared.py +1 -1
  65. package/hooks/blocking/code_rules_test_assertions.py +1 -1
  66. package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
  67. package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
  68. package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
  69. package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
  70. package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
  71. package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
  72. package/hooks/blocking/inventory_intent_records/records.py +271 -0
  73. package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
  74. package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
  75. package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
  76. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
  77. package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
  78. package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
  79. package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
  80. package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
  81. package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
  82. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
  83. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
  84. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
  85. package/hooks/blocking/pii_payload_scan.py +138 -42
  86. package/hooks/blocking/pii_prevention_blocker.py +185 -291
  87. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
  88. package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
  89. package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
  90. package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
  91. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
  92. package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
  93. package/hooks/blocking/pr_description_command_parser.py +8 -4
  94. package/hooks/blocking/precommit_code_rules_gate.py +3 -3
  95. package/hooks/blocking/tdd_enforcer.py +97 -608
  96. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
  97. package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
  98. package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
  99. package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
  100. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
  101. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
  102. package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
  103. package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
  104. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
  105. package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
  106. package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
  107. package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
  108. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
  109. package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
  110. package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
  111. package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
  112. package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
  113. package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
  114. package/hooks/blocking/test_pii_payload_scan.py +168 -0
  115. package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
  116. package/hooks/blocking/test_verifier_verdict_minter.py +55 -158
  117. package/hooks/blocking/tests/conftest.py +10 -0
  118. package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
  119. package/hooks/blocking/tests/test_repository_exemption.py +105 -0
  120. package/hooks/blocking/tests/test_repository_resolution.py +108 -0
  121. package/hooks/diagnostic/hook_log_extractor.py +12 -10
  122. package/hooks/git-hooks/post_commit.py +3 -4
  123. package/hooks/hooks_constants/CLAUDE.md +2 -2
  124. package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
  125. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  126. package/hooks/hooks_constants/local_identity.py +59 -8
  127. package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
  128. package/hooks/hooks_constants/test_local_identity.py +105 -3
  129. package/hooks/pyproject.toml +13 -36
  130. package/hooks/session/plugin_data_dir_cleanup.py +0 -1
  131. package/hooks/validation/mypy_validator.py +2 -2
  132. package/hooks/validators/health_check.py +1 -0
  133. package/hooks/validators/mypy_integration.py +2 -0
  134. package/hooks/validators/ruff_integration.py +3 -0
  135. package/hooks/workflow/auto_formatter.py +5 -4
  136. package/package.json +1 -1
  137. package/scripts/CLAUDE.md +4 -0
  138. package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
  139. package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
  140. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
  141. package/scripts/dev_env_scripts_constants/timing.py +7 -1
  142. package/scripts/grok_headless_runner.py +294 -0
  143. package/scripts/grok_worker_preflight.py +410 -0
  144. package/scripts/invoke_code_review.py +463 -0
  145. package/scripts/resolve_worker_spawn.py +619 -0
  146. package/scripts/spawn_grok_batch.py +672 -0
  147. package/scripts/test_grok_headless_runner.py +626 -0
  148. package/scripts/test_grok_worker_preflight.py +1054 -0
  149. package/scripts/test_invoke_code_review.py +672 -0
  150. package/scripts/test_resolve_worker_spawn.py +1014 -0
  151. package/scripts/test_spawn_grok_batch.py +1017 -0
  152. package/skills/CLAUDE.md +5 -3
  153. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
  154. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
  155. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
  156. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
  157. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
  158. package/skills/autoconverge/CLAUDE.md +3 -3
  159. package/skills/autoconverge/SKILL.md +9 -3
  160. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  161. package/skills/autoconverge/reference/convergence.md +33 -11
  162. package/skills/autoconverge/reference/stop-conditions.md +16 -5
  163. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  164. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
  165. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
  166. package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
  167. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
  168. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  169. package/skills/autoconverge/workflow/converge.mjs +200 -16
  170. package/skills/bugteam/CLAUDE.md +2 -2
  171. package/skills/bugteam/CONSTRAINTS.md +3 -2
  172. package/skills/bugteam/PROMPTS.md +7 -6
  173. package/skills/bugteam/SKILL.md +18 -13
  174. package/skills/bugteam/reference/audit-and-teammates.md +215 -35
  175. package/skills/bugteam/reference/design-rationale.md +1 -1
  176. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
  177. package/skills/bugteam/reference/team-setup.md +8 -2
  178. package/skills/codex-review/CLAUDE.md +46 -0
  179. package/skills/codex-review/SKILL.md +181 -0
  180. package/skills/codex-review/reference/CLAUDE.md +15 -0
  181. package/skills/codex-review/reference/cli-contract.md +253 -0
  182. package/skills/codex-review/reference/loop-integration.md +118 -0
  183. package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
  184. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
  185. package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
  186. package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
  187. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
  188. package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
  189. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
  190. package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
  191. package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
  192. package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
  193. package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
  194. package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
  195. package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
  196. package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
  197. package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
  198. package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
  199. package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
  200. package/skills/codex-review/scripts/run_codex_review.py +415 -0
  201. package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
  202. package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
  203. package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
  204. package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
  205. package/skills/codex-review/test_skill_scaffold.py +192 -0
  206. package/skills/grok-spawn/CLAUDE.md +28 -0
  207. package/skills/grok-spawn/SKILL.md +226 -0
  208. package/skills/grok-spawn/reference/flag-profiles.md +132 -0
  209. package/skills/grok-spawn/reference/worker-briefs.md +152 -0
  210. package/skills/grokify/SKILL.md +9 -1
  211. package/skills/grokify/capability-claims.test.mjs +28 -0
  212. package/skills/grokify/evals/README.md +72 -0
  213. package/skills/grokify/evals/parse-payload.test.mjs +171 -0
  214. package/skills/grokify/evals/run-capability-evals.mjs +545 -0
  215. package/skills/orchestrator/SKILL.md +42 -29
  216. package/skills/orchestrator-refresh/SKILL.md +17 -9
  217. package/skills/pr-converge/SKILL.md +34 -13
  218. package/skills/pr-converge/reference/convergence-gates.md +42 -15
  219. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  220. package/skills/pr-converge/reference/ground-rules.md +1 -1
  221. package/skills/pr-converge/reference/per-tick.md +130 -42
  222. package/skills/pr-converge/reference/state-schema.md +10 -0
  223. package/skills/pr-converge/scripts/CLAUDE.md +2 -0
  224. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
  225. package/skills/pr-converge/scripts/check_convergence.py +605 -29
  226. package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
  227. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
  228. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
  229. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
  230. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
  231. package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
  232. package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
  233. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
  234. package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
  235. package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
  236. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
  237. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
  238. package/skills/pr-converge/test_step5_host_branch.py +106 -0
  239. package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
  240. package/skills/reviewer-gates/SKILL.md +7 -5
  241. package/skills/team-advisor/SKILL.md +7 -7
@@ -0,0 +1,507 @@
1
+ """Fixture-driven tests for the conditional Codex convergence gate.
2
+
3
+ Covers: required-and-clean, required-and-dirty, skipped-by-threshold,
4
+ skipped-by-token, skipped-by-down. Threshold comes from the probe constant
5
+ via ``is_codex_review_required`` — this module never inlines the percent.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib.util
11
+ import json
12
+ import sys
13
+ from pathlib import Path
14
+ from types import ModuleType
15
+
16
+ import pytest
17
+
18
+ import _pr_converge_path_setup # noqa: F401
19
+ from codex_review_scripts_constants.codex_usage_probe_constants import (
20
+ WEEKLY_USAGE_GATE_THRESHOLD_PERCENT,
21
+ )
22
+ from pr_converge_scripts_constants.convergence_gate_constants import (
23
+ MINIMUM_ABBREVIATED_SHA_LENGTH,
24
+ )
25
+
26
+ _SCRIPTS_DIRECTORY = Path(__file__).absolute().parent
27
+ _PR_CONVERGE_DIRECTORY = _SCRIPTS_DIRECTORY.parent
28
+
29
+ CLEAN_LABEL = "Clean \u2014 no findings"
30
+ CLEAN_BODY = (
31
+ f"**Bugteam audit completed** \u2014 {CLEAN_LABEL}\n\n"
32
+ "No findings.\n"
33
+ )
34
+ HEAD_SHA = "ae8005aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
35
+ OTHER_SHA = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
36
+ PERCENT_ABOVE_THRESHOLD = float(WEEKLY_USAGE_GATE_THRESHOLD_PERCENT) + 1.0
37
+ PERCENT_AT_THRESHOLD = float(WEEKLY_USAGE_GATE_THRESHOLD_PERCENT)
38
+
39
+
40
+ def _load_module() -> ModuleType:
41
+ for each_cached_name in [
42
+ each_key
43
+ for each_key in list(sys.modules)
44
+ if each_key == "config" or each_key.startswith("config.")
45
+ ]:
46
+ sys.modules.pop(each_cached_name, None)
47
+ if str(_PR_CONVERGE_DIRECTORY) in sys.path:
48
+ sys.path.remove(str(_PR_CONVERGE_DIRECTORY))
49
+ sys.path.insert(0, str(_PR_CONVERGE_DIRECTORY))
50
+ module_path = _SCRIPTS_DIRECTORY / "check_convergence.py"
51
+ spec = importlib.util.spec_from_file_location(
52
+ "check_convergence_codex_under_test", module_path
53
+ )
54
+ assert spec is not None
55
+ assert spec.loader is not None
56
+ module = importlib.util.module_from_spec(spec)
57
+ spec.loader.exec_module(module)
58
+ return module
59
+
60
+
61
+ check_convergence = _load_module()
62
+
63
+
64
+ def _write_fixture(
65
+ tmp_path: Path,
66
+ *,
67
+ codex_percent_left: float | None,
68
+ codex_clean_at: str | None,
69
+ filename: str = "codex-gate.json",
70
+ ) -> Path:
71
+ fixture_path = tmp_path / filename
72
+ payload: dict[str, object] = {
73
+ "head_sha": HEAD_SHA,
74
+ "pr_object": {"mergeable": True, "mergeable_state": "clean"},
75
+ "reviews": [
76
+ {
77
+ "id": 1,
78
+ "body": CLEAN_BODY,
79
+ "commit_id": HEAD_SHA,
80
+ }
81
+ ],
82
+ "unresolved_bot_threads_passed": True,
83
+ "unresolved_bot_threads_detail": "0 unresolved",
84
+ "pending_reviews_passed": True,
85
+ "pending_reviews_detail": "none pending",
86
+ "codex_percent_left": codex_percent_left,
87
+ "codex_clean_at": codex_clean_at,
88
+ }
89
+ fixture_path.write_text(json.dumps(payload), encoding="utf-8")
90
+ return fixture_path
91
+
92
+
93
+ def should_pass_when_codex_required_and_clean(
94
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
95
+ ) -> None:
96
+ fixture_path = _write_fixture(
97
+ tmp_path,
98
+ codex_percent_left=PERCENT_ABOVE_THRESHOLD,
99
+ codex_clean_at=HEAD_SHA,
100
+ filename="required-clean.json",
101
+ )
102
+ exit_code = check_convergence.main(
103
+ [
104
+ "--owner",
105
+ "jl-cmd",
106
+ "--repo",
107
+ "claude-dev-env",
108
+ "--pr-number",
109
+ "111",
110
+ "--fixture",
111
+ str(fixture_path),
112
+ "--bugbot-down",
113
+ "--copilot-down",
114
+ ]
115
+ )
116
+ captured = capsys.readouterr().out
117
+ assert exit_code == 0
118
+ assert "codex_clean_at == current_head: PASS" in captured
119
+ assert "clean at" in captured
120
+ assert "All pre-conditions met" in captured
121
+
122
+
123
+ def should_pass_without_reading_usage_when_the_clean_stamp_is_on_head(
124
+ monkeypatch: pytest.MonkeyPatch,
125
+ ) -> None:
126
+ def _probe_that_must_not_run() -> float | None:
127
+ raise AssertionError(
128
+ "the weekly usage probe must not run when codex_clean_at is on HEAD"
129
+ )
130
+
131
+ monkeypatch.setattr(
132
+ check_convergence, "_probe_codex_percent_left", _probe_that_must_not_run
133
+ )
134
+
135
+ is_passed, detail = check_convergence._evaluate_codex_clean(
136
+ read_percent_left=check_convergence._probe_codex_percent_left,
137
+ codex_clean_at=HEAD_SHA,
138
+ head_sha=HEAD_SHA,
139
+ )
140
+
141
+ assert is_passed is True
142
+ assert "clean at" in detail
143
+
144
+
145
+ def should_match_a_clean_stamp_whose_hex_case_differs_from_the_head_sha() -> None:
146
+ assert check_convergence._sha_matches_head(HEAD_SHA.upper(), HEAD_SHA) is True
147
+
148
+
149
+ def should_fail_when_codex_clean_stamp_is_shorter_than_an_abbreviated_sha(
150
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
151
+ ) -> None:
152
+ fixture_path = _write_fixture(
153
+ tmp_path,
154
+ codex_percent_left=PERCENT_ABOVE_THRESHOLD,
155
+ codex_clean_at=HEAD_SHA[:1],
156
+ filename="required-short-stamp.json",
157
+ )
158
+ exit_code = check_convergence.main(
159
+ [
160
+ "--owner",
161
+ "jl-cmd",
162
+ "--repo",
163
+ "claude-dev-env",
164
+ "--pr-number",
165
+ "111",
166
+ "--fixture",
167
+ str(fixture_path),
168
+ "--bugbot-down",
169
+ "--copilot-down",
170
+ ]
171
+ )
172
+ captured = capsys.readouterr().out
173
+ assert exit_code == 1
174
+ assert "codex_clean_at == current_head: FAIL" in captured
175
+
176
+
177
+ def should_pass_when_codex_clean_stamp_is_an_abbreviated_head_sha(
178
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
179
+ ) -> None:
180
+ fixture_path = _write_fixture(
181
+ tmp_path,
182
+ codex_percent_left=PERCENT_ABOVE_THRESHOLD,
183
+ codex_clean_at=HEAD_SHA[:MINIMUM_ABBREVIATED_SHA_LENGTH],
184
+ filename="required-abbreviated-stamp.json",
185
+ )
186
+ exit_code = check_convergence.main(
187
+ [
188
+ "--owner",
189
+ "jl-cmd",
190
+ "--repo",
191
+ "claude-dev-env",
192
+ "--pr-number",
193
+ "111",
194
+ "--fixture",
195
+ str(fixture_path),
196
+ "--bugbot-down",
197
+ "--copilot-down",
198
+ ]
199
+ )
200
+ captured = capsys.readouterr().out
201
+ assert exit_code == 0
202
+ assert "codex_clean_at == current_head: PASS" in captured
203
+
204
+
205
+ def should_fail_when_codex_required_and_dirty(
206
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
207
+ ) -> None:
208
+ fixture_path = _write_fixture(
209
+ tmp_path,
210
+ codex_percent_left=PERCENT_ABOVE_THRESHOLD,
211
+ codex_clean_at=OTHER_SHA,
212
+ filename="required-dirty.json",
213
+ )
214
+ exit_code = check_convergence.main(
215
+ [
216
+ "--owner",
217
+ "jl-cmd",
218
+ "--repo",
219
+ "claude-dev-env",
220
+ "--pr-number",
221
+ "111",
222
+ "--fixture",
223
+ str(fixture_path),
224
+ "--bugbot-down",
225
+ "--copilot-down",
226
+ ]
227
+ )
228
+ captured = capsys.readouterr().out
229
+ assert exit_code == 1
230
+ assert "codex_clean_at == current_head: FAIL" in captured
231
+ assert "no codex clean on" in captured
232
+ assert "do not mark ready" in captured
233
+
234
+
235
+ def should_skip_when_usage_at_or_below_threshold(
236
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
237
+ ) -> None:
238
+ fixture_path = _write_fixture(
239
+ tmp_path,
240
+ codex_percent_left=PERCENT_AT_THRESHOLD,
241
+ codex_clean_at=None,
242
+ filename="skipped-threshold.json",
243
+ )
244
+ exit_code = check_convergence.main(
245
+ [
246
+ "--owner",
247
+ "jl-cmd",
248
+ "--repo",
249
+ "claude-dev-env",
250
+ "--pr-number",
251
+ "111",
252
+ "--fixture",
253
+ str(fixture_path),
254
+ "--bugbot-down",
255
+ "--copilot-down",
256
+ ]
257
+ )
258
+ captured = capsys.readouterr().out
259
+ assert exit_code == 0
260
+ assert "codex_clean_at == current_head: PASS" in captured
261
+ assert "skipped (codex review not required)" in captured
262
+
263
+
264
+ def should_skip_when_codex_percent_left_is_null(
265
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
266
+ ) -> None:
267
+ fixture_path = _write_fixture(
268
+ tmp_path,
269
+ codex_percent_left=None,
270
+ codex_clean_at=None,
271
+ filename="skipped-null.json",
272
+ )
273
+ exit_code = check_convergence.main(
274
+ [
275
+ "--owner",
276
+ "jl-cmd",
277
+ "--repo",
278
+ "claude-dev-env",
279
+ "--pr-number",
280
+ "111",
281
+ "--fixture",
282
+ str(fixture_path),
283
+ "--bugbot-down",
284
+ "--copilot-down",
285
+ ]
286
+ )
287
+ captured = capsys.readouterr().out
288
+ assert exit_code == 0
289
+ assert "skipped (codex review not required)" in captured
290
+
291
+
292
+ def should_bypass_when_codex_token_disables_reviewer(
293
+ tmp_path: Path,
294
+ monkeypatch: pytest.MonkeyPatch,
295
+ capsys: pytest.CaptureFixture[str],
296
+ ) -> None:
297
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "codex")
298
+ fixture_path = _write_fixture(
299
+ tmp_path,
300
+ codex_percent_left=PERCENT_ABOVE_THRESHOLD,
301
+ codex_clean_at=None,
302
+ filename="skipped-token.json",
303
+ )
304
+ exit_code = check_convergence.main(
305
+ [
306
+ "--owner",
307
+ "jl-cmd",
308
+ "--repo",
309
+ "claude-dev-env",
310
+ "--pr-number",
311
+ "111",
312
+ "--fixture",
313
+ str(fixture_path),
314
+ "--bugbot-down",
315
+ "--copilot-down",
316
+ ]
317
+ )
318
+ captured = capsys.readouterr().out
319
+ assert exit_code == 0
320
+ assert "bypassed (codex_down)" in captured
321
+ assert "no codex clean on" not in captured
322
+
323
+
324
+ def should_bypass_when_codex_down_flag_set(
325
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
326
+ ) -> None:
327
+ fixture_path = _write_fixture(
328
+ tmp_path,
329
+ codex_percent_left=PERCENT_ABOVE_THRESHOLD,
330
+ codex_clean_at=None,
331
+ filename="skipped-down.json",
332
+ )
333
+ exit_code = check_convergence.main(
334
+ [
335
+ "--owner",
336
+ "jl-cmd",
337
+ "--repo",
338
+ "claude-dev-env",
339
+ "--pr-number",
340
+ "111",
341
+ "--fixture",
342
+ str(fixture_path),
343
+ "--bugbot-down",
344
+ "--copilot-down",
345
+ "--codex-down",
346
+ ]
347
+ )
348
+ captured = capsys.readouterr().out
349
+ assert exit_code == 0
350
+ assert "bypassed (codex_down)" in captured
351
+
352
+
353
+ def should_resolve_codex_down_true_when_flag_set(
354
+ monkeypatch: pytest.MonkeyPatch,
355
+ ) -> None:
356
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
357
+ monkeypatch.delenv("CLAUDE_JOB_DIR", raising=False)
358
+ assert check_convergence._resolve_codex_down(
359
+ True, check_convergence._read_job_state()
360
+ ) is True
361
+
362
+
363
+ def should_resolve_codex_down_true_when_env_disables_codex(
364
+ monkeypatch: pytest.MonkeyPatch,
365
+ ) -> None:
366
+ monkeypatch.delenv("CLAUDE_JOB_DIR", raising=False)
367
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "codex")
368
+ assert check_convergence._resolve_codex_down(
369
+ False, check_convergence._read_job_state()
370
+ ) is True
371
+
372
+
373
+ def should_resolve_codex_down_false_when_flag_unset_and_env_empty(
374
+ monkeypatch: pytest.MonkeyPatch,
375
+ ) -> None:
376
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
377
+ monkeypatch.delenv("CLAUDE_JOB_DIR", raising=False)
378
+ assert check_convergence._resolve_codex_down(
379
+ False, check_convergence._read_job_state()
380
+ ) is False
381
+
382
+
383
+ def should_resolve_codex_down_true_when_job_state_sticky_down(
384
+ tmp_path: Path,
385
+ monkeypatch: pytest.MonkeyPatch,
386
+ ) -> None:
387
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
388
+ monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
389
+ (tmp_path / "pr-converge-state.json").write_text(
390
+ json.dumps({"codex_down": True}),
391
+ encoding="utf-8",
392
+ )
393
+ assert check_convergence._resolve_codex_down(
394
+ False, check_convergence._read_job_state()
395
+ ) is True
396
+
397
+
398
+ def should_resolve_codex_down_false_when_job_state_codex_down_is_false(
399
+ tmp_path: Path,
400
+ monkeypatch: pytest.MonkeyPatch,
401
+ ) -> None:
402
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
403
+ monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
404
+ (tmp_path / "pr-converge-state.json").write_text(
405
+ json.dumps({"codex_down": False}),
406
+ encoding="utf-8",
407
+ )
408
+ assert check_convergence._resolve_codex_down(
409
+ False, check_convergence._read_job_state()
410
+ ) is False
411
+
412
+
413
+ def should_bypass_when_job_state_codex_down_is_true(
414
+ tmp_path: Path,
415
+ monkeypatch: pytest.MonkeyPatch,
416
+ capsys: pytest.CaptureFixture[str],
417
+ ) -> None:
418
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
419
+ monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
420
+ (tmp_path / "pr-converge-state.json").write_text(
421
+ json.dumps({"codex_down": True}),
422
+ encoding="utf-8",
423
+ )
424
+ fixture_path = _write_fixture(
425
+ tmp_path,
426
+ codex_percent_left=PERCENT_ABOVE_THRESHOLD,
427
+ codex_clean_at=None,
428
+ filename="job-state-codex-down.json",
429
+ )
430
+ exit_code = check_convergence.main(
431
+ [
432
+ "--owner",
433
+ "jl-cmd",
434
+ "--repo",
435
+ "claude-dev-env",
436
+ "--pr-number",
437
+ "111",
438
+ "--fixture",
439
+ str(fixture_path),
440
+ "--bugbot-down",
441
+ "--copilot-down",
442
+ ]
443
+ )
444
+ captured = capsys.readouterr().out
445
+ assert exit_code == 0
446
+ assert "bypassed (codex_down)" in captured
447
+ assert "no codex clean on" not in captured
448
+
449
+
450
+ def should_fail_when_job_state_lacks_codex_down_and_clean_stamp(
451
+ tmp_path: Path,
452
+ monkeypatch: pytest.MonkeyPatch,
453
+ capsys: pytest.CaptureFixture[str],
454
+ ) -> None:
455
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
456
+ monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
457
+ (tmp_path / "pr-converge-state.json").write_text(
458
+ json.dumps({}),
459
+ encoding="utf-8",
460
+ )
461
+ fixture_path = _write_fixture(
462
+ tmp_path,
463
+ codex_percent_left=PERCENT_ABOVE_THRESHOLD,
464
+ codex_clean_at=None,
465
+ filename="job-state-no-down.json",
466
+ )
467
+ exit_code = check_convergence.main(
468
+ [
469
+ "--owner",
470
+ "jl-cmd",
471
+ "--repo",
472
+ "claude-dev-env",
473
+ "--pr-number",
474
+ "111",
475
+ "--fixture",
476
+ str(fixture_path),
477
+ "--bugbot-down",
478
+ "--copilot-down",
479
+ ]
480
+ )
481
+ captured = capsys.readouterr().out
482
+ assert exit_code == 1
483
+ assert "codex_clean_at == current_head: FAIL" in captured
484
+ assert "no codex clean on" in captured
485
+
486
+
487
+ def should_accept_codex_down_flag_in_parsed_arguments() -> None:
488
+ arguments = check_convergence.parse_arguments(
489
+ ["--owner", "o", "--repo", "r", "--pr-number", "1", "--codex-down"]
490
+ )
491
+ assert arguments.codex_down is True
492
+
493
+
494
+ def should_accept_codex_clean_at_flag_in_parsed_arguments() -> None:
495
+ arguments = check_convergence.parse_arguments(
496
+ [
497
+ "--owner",
498
+ "o",
499
+ "--repo",
500
+ "r",
501
+ "--pr-number",
502
+ "1",
503
+ "--codex-clean-at",
504
+ HEAD_SHA,
505
+ ]
506
+ )
507
+ assert arguments.codex_clean_at == HEAD_SHA