claude-dev-env 1.94.0 → 2.0.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 (280) hide show
  1. package/_shared/advisor/CLAUDE.md +2 -2
  2. package/_shared/advisor/advisor-protocol.md +41 -33
  3. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +17 -13
  4. package/_shared/advisor/scripts/model_tier_run_validator.py +27 -18
  5. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +95 -25
  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/CLAUDE.md +10 -1
  42. package/bin/ever-shipped-skills.mjs +73 -0
  43. package/bin/expand_home_directory_tokens.mjs +1 -1
  44. package/bin/install.mjs +140 -7
  45. package/bin/install.prune.test.mjs +457 -0
  46. package/docs/CODE_RULES.md +1 -1
  47. package/hooks/advisory/refactor_guard.py +3 -4
  48. package/hooks/blocking/CLAUDE.md +7 -1
  49. package/hooks/blocking/block_main_commit.py +2 -2
  50. package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
  51. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
  52. package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
  53. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
  54. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
  55. package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
  56. package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
  57. package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
  58. package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
  59. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
  60. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
  61. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
  62. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
  63. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
  64. package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
  65. package/hooks/blocking/code_rules_enforcer.py +4 -0
  66. package/hooks/blocking/code_rules_mock_completeness.py +1 -1
  67. package/hooks/blocking/code_rules_optional_params.py +2 -2
  68. package/hooks/blocking/code_rules_shared.py +83 -1
  69. package/hooks/blocking/code_rules_test_assertions.py +1 -1
  70. package/hooks/blocking/code_rules_test_layout.py +9 -3
  71. package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
  72. package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
  73. package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
  74. package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
  75. package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
  76. package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
  77. package/hooks/blocking/inventory_intent_records/records.py +271 -0
  78. package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
  79. package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
  80. package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
  81. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
  82. package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
  83. package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
  84. package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
  85. package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
  86. package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
  87. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
  88. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
  89. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
  90. package/hooks/blocking/pii_payload_scan.py +138 -42
  91. package/hooks/blocking/pii_prevention_blocker.py +185 -291
  92. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
  93. package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
  94. package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
  95. package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
  96. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
  97. package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
  98. package/hooks/blocking/plain_language_blocker.py +138 -4
  99. package/hooks/blocking/pr_description_command_parser.py +8 -4
  100. package/hooks/blocking/precommit_code_rules_gate.py +3 -3
  101. package/hooks/blocking/sensitive_file_protector.py +114 -48
  102. package/hooks/blocking/tdd_enforcer.py +97 -601
  103. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
  104. package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
  105. package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
  106. package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
  107. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
  108. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
  109. package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
  110. package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
  111. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
  112. package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
  113. package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
  114. package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
  115. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
  116. package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
  117. package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
  118. package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
  119. package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
  120. package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +105 -0
  121. package/hooks/blocking/test_code_rules_shared.py +181 -0
  122. package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
  123. package/hooks/blocking/test_pii_payload_scan.py +168 -0
  124. package/hooks/blocking/test_plain_language_blocker_allowlist.py +184 -0
  125. package/hooks/blocking/test_sensitive_file_protector.py +185 -0
  126. package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
  127. package/hooks/blocking/test_tdd_enforcer_scratchpad.py +105 -0
  128. package/hooks/blocking/tests/conftest.py +10 -0
  129. package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
  130. package/hooks/blocking/tests/test_repository_exemption.py +105 -0
  131. package/hooks/blocking/tests/test_repository_resolution.py +108 -0
  132. package/hooks/diagnostic/hook_log_extractor.py +12 -10
  133. package/hooks/git-hooks/post_commit.py +3 -4
  134. package/hooks/hooks_constants/CLAUDE.md +4 -2
  135. package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
  136. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  137. package/hooks/hooks_constants/harness_scratchpad_constants.py +17 -0
  138. package/hooks/hooks_constants/local_identity.py +59 -8
  139. package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
  140. package/hooks/hooks_constants/plain_language_blocker_constants.py +5 -0
  141. package/hooks/hooks_constants/sensitive_file_protector_constants.py +42 -0
  142. package/hooks/hooks_constants/test_local_identity.py +105 -3
  143. package/hooks/pyproject.toml +52 -4
  144. package/hooks/session/plugin_data_dir_cleanup.py +0 -1
  145. package/hooks/validation/mypy_validator.py +2 -2
  146. package/hooks/validators/CLAUDE.md +1 -1
  147. package/hooks/validators/README.md +2 -0
  148. package/hooks/validators/health_check.py +1 -0
  149. package/hooks/validators/mypy_integration.py +2 -0
  150. package/hooks/validators/python_style_checks.py +114 -136
  151. package/hooks/validators/python_style_helpers.py +95 -0
  152. package/hooks/validators/ruff_integration.py +3 -0
  153. package/hooks/validators/test_python_style_checks.py +0 -164
  154. package/hooks/validators/test_python_style_checks_decorator_gap.py +119 -0
  155. package/hooks/validators/test_python_style_fixes.py +251 -0
  156. package/hooks/validators/test_python_style_helpers.py +125 -0
  157. package/hooks/workflow/auto_formatter.py +5 -4
  158. package/package.json +1 -1
  159. package/rules/CLAUDE.md +1 -0
  160. package/rules/anti-corollary-tests.md +69 -0
  161. package/rules/bdd.md +1 -3
  162. package/rules/code-reviews.md +1 -1
  163. package/rules/gh-paginate.md +1 -1
  164. package/rules/plain-language.md +2 -0
  165. package/scripts/CLAUDE.md +4 -0
  166. package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
  167. package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
  168. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
  169. package/scripts/dev_env_scripts_constants/timing.py +7 -1
  170. package/scripts/grok_headless_runner.py +294 -0
  171. package/scripts/grok_worker_preflight.py +410 -0
  172. package/scripts/invoke_code_review.py +463 -0
  173. package/scripts/resolve_worker_spawn.py +619 -0
  174. package/scripts/spawn_grok_batch.py +672 -0
  175. package/scripts/test_grok_headless_runner.py +626 -0
  176. package/scripts/test_grok_worker_preflight.py +1054 -0
  177. package/scripts/test_invoke_code_review.py +672 -0
  178. package/scripts/test_resolve_worker_spawn.py +1014 -0
  179. package/scripts/test_spawn_grok_batch.py +1017 -0
  180. package/skills/CLAUDE.md +6 -3
  181. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
  182. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
  183. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
  184. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
  185. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
  186. package/skills/autoconverge/CLAUDE.md +3 -3
  187. package/skills/autoconverge/SKILL.md +9 -3
  188. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  189. package/skills/autoconverge/reference/convergence.md +33 -11
  190. package/skills/autoconverge/reference/stop-conditions.md +16 -5
  191. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  192. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
  193. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
  194. package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
  195. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
  196. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  197. package/skills/autoconverge/workflow/converge.mjs +202 -18
  198. package/skills/bugteam/CLAUDE.md +2 -2
  199. package/skills/bugteam/CONSTRAINTS.md +3 -2
  200. package/skills/bugteam/PROMPTS.md +7 -6
  201. package/skills/bugteam/SKILL.md +18 -13
  202. package/skills/bugteam/reference/README.md +2 -3
  203. package/skills/bugteam/reference/audit-and-teammates.md +215 -35
  204. package/skills/bugteam/reference/design-rationale.md +1 -1
  205. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
  206. package/skills/bugteam/reference/team-setup.md +8 -2
  207. package/skills/closeout/SKILL.md +153 -0
  208. package/skills/closeout/reference/handoff-prompt-template.md +72 -0
  209. package/skills/closeout/reference/issue-body-templates.md +108 -0
  210. package/skills/closeout/reference/pii-redaction-checklist.md +36 -0
  211. package/skills/codex-review/CLAUDE.md +46 -0
  212. package/skills/codex-review/SKILL.md +181 -0
  213. package/skills/codex-review/reference/CLAUDE.md +15 -0
  214. package/skills/codex-review/reference/cli-contract.md +253 -0
  215. package/skills/codex-review/reference/loop-integration.md +118 -0
  216. package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
  217. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
  218. package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
  219. package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
  220. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
  221. package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
  222. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
  223. package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
  224. package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
  225. package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
  226. package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
  227. package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
  228. package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
  229. package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
  230. package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
  231. package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
  232. package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
  233. package/skills/codex-review/scripts/run_codex_review.py +415 -0
  234. package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
  235. package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
  236. package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
  237. package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
  238. package/skills/codex-review/test_skill_scaffold.py +192 -0
  239. package/skills/grok-spawn/CLAUDE.md +28 -0
  240. package/skills/grok-spawn/SKILL.md +226 -0
  241. package/skills/grok-spawn/reference/flag-profiles.md +132 -0
  242. package/skills/grok-spawn/reference/worker-briefs.md +152 -0
  243. package/skills/grokify/SKILL.md +9 -1
  244. package/skills/grokify/capability-claims.test.mjs +28 -0
  245. package/skills/grokify/evals/README.md +72 -0
  246. package/skills/grokify/evals/parse-payload.test.mjs +171 -0
  247. package/skills/grokify/evals/run-capability-evals.mjs +545 -0
  248. package/skills/orchestrator/SKILL.md +32 -22
  249. package/skills/orchestrator-refresh/SKILL.md +12 -8
  250. package/skills/pr-converge/CLAUDE.md +1 -1
  251. package/skills/pr-converge/SKILL.md +34 -13
  252. package/skills/pr-converge/reference/convergence-gates.md +42 -15
  253. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  254. package/skills/pr-converge/reference/ground-rules.md +1 -1
  255. package/skills/pr-converge/reference/per-tick.md +130 -42
  256. package/skills/pr-converge/reference/state-schema.md +10 -0
  257. package/skills/pr-converge/scripts/CLAUDE.md +2 -0
  258. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
  259. package/skills/pr-converge/scripts/check_convergence.py +605 -29
  260. package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
  261. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
  262. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
  263. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
  264. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
  265. package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
  266. package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
  267. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
  268. package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
  269. package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
  270. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
  271. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
  272. package/skills/pr-converge/test_step5_host_branch.py +106 -0
  273. package/skills/pr-fix-protocol/SKILL.md +65 -0
  274. package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
  275. package/skills/reviewer-gates/SKILL.md +7 -5
  276. package/skills/skill-builder/references/skill-modularity.md +1 -1
  277. package/skills/team-advisor/SKILL.md +16 -12
  278. package/system-prompts/software-engineer.xml +7 -6
  279. package/hooks/validators/test_verify_paths.py +0 -32
  280. package/hooks/validators/verify_paths.py +0 -57
@@ -17,6 +17,7 @@ from typing import Callable
17
17
  import pytest
18
18
 
19
19
  import _pr_converge_path_setup # noqa: F401
20
+ import check_convergence_availability as availability_module
20
21
  from pr_converge_skill_constants.constants import EXIT_CODE_GH_ERROR
21
22
 
22
23
  _SCRIPTS_DIRECTORY = Path(__file__).absolute().parent
@@ -106,8 +107,15 @@ def _raise_if_called(gate_label: str) -> Callable[..., tuple[bool, str]]:
106
107
  return _stub
107
108
 
108
109
 
110
+ def _null_codex_percent_left() -> float | None:
111
+ return None
112
+
113
+
109
114
  def _patch_all_gates_passing(monkeypatch: pytest.MonkeyPatch) -> None:
110
115
  monkeypatch.setattr(check_convergence, "_get_pr_head_sha", _passing_head_sha)
116
+ monkeypatch.setattr(
117
+ check_convergence, "_probe_codex_percent_left", _null_codex_percent_left
118
+ )
111
119
  for each_gate_name in _ALL_LEAF_GATE_NAMES:
112
120
  monkeypatch.setattr(check_convergence, each_gate_name, _passing_gate)
113
121
 
@@ -282,7 +290,14 @@ def should_bypass_bugbot_gates_when_bugbot_down_is_true(
282
290
  check_convergence, "_check_bugbot_not_dirty", _raise_if_called("_check_bugbot_not_dirty")
283
291
  )
284
292
  exit_code = check_convergence.check_all(
285
- owner="o", repo="r", number=1, is_bugbot_down=True, is_copilot_down=False
293
+ owner="o",
294
+ repo="r",
295
+ number=1,
296
+ is_bugbot_down=True,
297
+ is_copilot_down=False,
298
+ is_bugteam_post_blocked=False,
299
+ is_codex_down=False,
300
+ live_codex_clean_at=None,
286
301
  )
287
302
  assert "bypassed (bugbot_down)" in capsys.readouterr().out
288
303
  assert exit_code == 0
@@ -297,7 +312,14 @@ def should_bypass_copilot_gates_when_copilot_down_is_true(
297
312
  check_convergence, "_check_no_pending_reviews", _raise_if_called("_check_no_pending_reviews")
298
313
  )
299
314
  exit_code = check_convergence.check_all(
300
- owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=True
315
+ owner="o",
316
+ repo="r",
317
+ number=1,
318
+ is_bugbot_down=False,
319
+ is_copilot_down=True,
320
+ is_bugteam_post_blocked=False,
321
+ is_codex_down=False,
322
+ live_codex_clean_at=None,
301
323
  )
302
324
  assert "bypassed (copilot_down)" in capsys.readouterr().out
303
325
  assert exit_code == 0
@@ -341,7 +363,14 @@ def should_propagate_systemexit_from_get_pr_head_sha(monkeypatch: pytest.MonkeyP
341
363
  )
342
364
  with pytest.raises(SystemExit) as exc_info:
343
365
  check_convergence.check_all(
344
- owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=False
366
+ owner="o",
367
+ repo="r",
368
+ number=1,
369
+ is_bugbot_down=False,
370
+ is_copilot_down=False,
371
+ is_bugteam_post_blocked=False,
372
+ is_codex_down=False,
373
+ live_codex_clean_at=None,
345
374
  )
346
375
  assert exc_info.value.code == EXIT_CODE_GH_ERROR
347
376
 
@@ -350,6 +379,12 @@ def should_derive_copilot_down_from_env_when_main_omits_the_flag(
350
379
  monkeypatch: pytest.MonkeyPatch,
351
380
  ) -> None:
352
381
  monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "copilot")
382
+ monkeypatch.setattr(
383
+ availability_module,
384
+ "get_claude_user_settings_path",
385
+ lambda: Path("does-not-exist"),
386
+ )
387
+ availability_module._log_disk_settings_fallback_once.cache_clear()
353
388
  captured_copilot_down: list[bool] = []
354
389
 
355
390
  def stub_check_all(
@@ -360,6 +395,9 @@ def should_derive_copilot_down_from_env_when_main_omits_the_flag(
360
395
  is_bugbot_down: bool,
361
396
  is_copilot_down: bool,
362
397
  is_bugteam_post_blocked: bool = False,
398
+ is_codex_down: bool = False,
399
+ live_codex_clean_at: str | None = None,
400
+ **_bypass_note_kwargs: str,
363
401
  ) -> int:
364
402
  captured_copilot_down.append(is_copilot_down)
365
403
  return 0
@@ -368,3 +406,33 @@ def should_derive_copilot_down_from_env_when_main_omits_the_flag(
368
406
  exit_code = check_convergence.main(["--owner", "o", "--repo", "r", "--pr-number", "1"])
369
407
  assert exit_code == 0
370
408
  assert captured_copilot_down == [True]
409
+
410
+
411
+ def should_print_probe_reason_on_the_bypassed_copilot_gate_line(
412
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
413
+ ) -> None:
414
+ _patch_all_gates_passing(monkeypatch)
415
+ monkeypatch.setattr(
416
+ check_convergence, "_check_bot_review", _raise_if_called("_check_bot_review")
417
+ )
418
+ monkeypatch.setattr(
419
+ check_convergence,
420
+ "_check_no_pending_reviews",
421
+ _raise_if_called("_check_no_pending_reviews"),
422
+ )
423
+ exit_code = check_convergence.check_all(
424
+ owner="o",
425
+ repo="r",
426
+ number=1,
427
+ is_bugbot_down=False,
428
+ is_copilot_down=True,
429
+ is_bugteam_post_blocked=False,
430
+ is_codex_down=False,
431
+ live_codex_clean_at=None,
432
+ copilot_bypass_note="copilot unavailable: out of premium quota",
433
+ )
434
+ captured_stdout = capsys.readouterr().out
435
+ probe_line = "bypassed (copilot unavailable: out of premium quota)"
436
+ assert captured_stdout.count(probe_line) == 2
437
+ assert exit_code == 0
438
+
@@ -0,0 +1,326 @@
1
+ """Tests for the reviewer-availability + settings resolution of the convergence gate.
2
+
3
+ ::
4
+
5
+ probe out-of-quota ok: waived, "copilot unavailable: <reason>"
6
+ probe available flag: enforced, no note
7
+ probe API down flag: enforced, probe_error_reason set (not waived)
8
+ probe exception flag: enforced, probe_error_reason set
9
+ disk lists copilot ok: waived even when env omits it (disk authoritative)
10
+ disk omits copilot flag: enforced even when env lists it (disk authoritative)
11
+ disk unreadable ok: env fallback, logged once per process
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import importlib.util
17
+ import json
18
+ import logging
19
+ import sys
20
+ from pathlib import Path
21
+ from types import ModuleType
22
+ from typing import Callable
23
+
24
+ import pytest
25
+
26
+ import _pr_converge_path_setup # noqa: F401
27
+ from pr_loop_shared_constants.copilot_quota_constants import (
28
+ EXIT_CODE_NO_ACCOUNT_CONFIGURED,
29
+ EXIT_CODE_OUT_OF_QUOTA,
30
+ EXIT_CODE_QUOTA_API_DOWN,
31
+ EXIT_CODE_QUOTA_AVAILABLE,
32
+ )
33
+
34
+ _SCRIPTS_DIRECTORY = Path(__file__).absolute().parent
35
+
36
+
37
+ def _load_availability() -> ModuleType:
38
+ if str(_SCRIPTS_DIRECTORY) not in sys.path:
39
+ sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
40
+ module_path = _SCRIPTS_DIRECTORY / "check_convergence_availability.py"
41
+ spec = importlib.util.spec_from_file_location(
42
+ "check_convergence_availability_under_test", module_path
43
+ )
44
+ assert spec is not None
45
+ assert spec.loader is not None
46
+ module = importlib.util.module_from_spec(spec)
47
+ spec.loader.exec_module(module)
48
+ return module
49
+
50
+
51
+ availability = _load_availability()
52
+
53
+
54
+ class _StubQuotaDecision:
55
+ """Stand-in for QuotaDecision the copilot probe reads exit_code and message off."""
56
+
57
+ def __init__(self, exit_code: int, message: str) -> None:
58
+ self.exit_code = exit_code
59
+ self.message = message
60
+
61
+
62
+ def _stub_quota(exit_code: int, message: str) -> Callable[..., _StubQuotaDecision]:
63
+ def _probe(**_call_keywords: object) -> _StubQuotaDecision:
64
+ return _StubQuotaDecision(exit_code, message)
65
+
66
+ return _probe
67
+
68
+
69
+ def _write_settings(
70
+ tmp_path: Path,
71
+ *,
72
+ reviews_disabled_value: str | None = None,
73
+ reviews_enabled_value: str | None = None,
74
+ ) -> Path:
75
+ env_block: dict[str, str] = {}
76
+ if reviews_disabled_value is not None:
77
+ env_block["CLAUDE_REVIEWS_DISABLED"] = reviews_disabled_value
78
+ if reviews_enabled_value is not None:
79
+ env_block["CLAUDE_REVIEWS_ENABLED"] = reviews_enabled_value
80
+ settings_path = tmp_path / "settings.json"
81
+ settings_path.write_text(json.dumps({"env": env_block}), encoding="utf-8")
82
+ return settings_path
83
+
84
+
85
+ @pytest.fixture(autouse=True)
86
+ def _reset_probe_cache_and_env(monkeypatch: pytest.MonkeyPatch) -> None:
87
+ availability._probe_copilot_quota.cache_clear()
88
+ availability._log_disk_settings_fallback_once.cache_clear()
89
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
90
+ monkeypatch.delenv("CLAUDE_REVIEWS_ENABLED", raising=False)
91
+ monkeypatch.setattr(
92
+ availability,
93
+ "get_claude_user_settings_path",
94
+ lambda: Path("does-not-exist"),
95
+ )
96
+
97
+
98
+ def should_copilot_probe_waive_on_out_of_quota(
99
+ monkeypatch: pytest.MonkeyPatch,
100
+ ) -> None:
101
+ monkeypatch.setattr(
102
+ availability,
103
+ "evaluate_copilot_quota",
104
+ _stub_quota(EXIT_CODE_OUT_OF_QUOTA, "out of premium quota"),
105
+ )
106
+ waiver = availability._probe_copilot_quota()
107
+ assert waiver.is_waived is True
108
+ assert waiver.bypass_note == "copilot unavailable: out of premium quota"
109
+ assert waiver.probe_error_reason == ""
110
+
111
+
112
+ def should_copilot_probe_enforce_when_quota_available(
113
+ monkeypatch: pytest.MonkeyPatch,
114
+ ) -> None:
115
+ monkeypatch.setattr(
116
+ availability,
117
+ "evaluate_copilot_quota",
118
+ _stub_quota(EXIT_CODE_QUOTA_AVAILABLE, "quota available"),
119
+ )
120
+ waiver = availability._probe_copilot_quota()
121
+ assert waiver.is_waived is False
122
+ assert waiver.bypass_note == ""
123
+ assert waiver.probe_error_reason == ""
124
+
125
+
126
+ def should_copilot_probe_set_probe_error_on_quota_api_down(
127
+ monkeypatch: pytest.MonkeyPatch,
128
+ ) -> None:
129
+ monkeypatch.setattr(
130
+ availability,
131
+ "evaluate_copilot_quota",
132
+ _stub_quota(EXIT_CODE_QUOTA_API_DOWN, "quota API unreachable"),
133
+ )
134
+ waiver = availability._probe_copilot_quota()
135
+ assert waiver.is_waived is False
136
+ assert waiver.bypass_note == ""
137
+ assert waiver.probe_error_reason == "quota API unreachable"
138
+
139
+
140
+ def should_copilot_probe_set_probe_error_on_no_account_configured(
141
+ monkeypatch: pytest.MonkeyPatch,
142
+ ) -> None:
143
+ monkeypatch.setattr(
144
+ availability,
145
+ "evaluate_copilot_quota",
146
+ _stub_quota(
147
+ EXIT_CODE_NO_ACCOUNT_CONFIGURED,
148
+ "no account configured",
149
+ ),
150
+ )
151
+ waiver = availability._probe_copilot_quota()
152
+ assert waiver.is_waived is False
153
+ assert waiver.bypass_note == ""
154
+ assert waiver.probe_error_reason == "no account configured"
155
+
156
+
157
+ def should_copilot_probe_set_probe_error_on_exception(
158
+ monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
159
+ ) -> None:
160
+ def _raising_probe(**_call_keywords: object) -> object:
161
+ raise RuntimeError("quota API unreachable")
162
+
163
+ monkeypatch.setattr(availability, "evaluate_copilot_quota", _raising_probe)
164
+ with caplog.at_level(logging.WARNING):
165
+ waiver = availability._probe_copilot_quota()
166
+ assert waiver.is_waived is False
167
+ assert waiver.probe_error_reason == "quota API unreachable"
168
+ assert "probe error" in caplog.text
169
+
170
+
171
+ def should_probe_copilot_at_most_once_per_run(
172
+ monkeypatch: pytest.MonkeyPatch,
173
+ ) -> None:
174
+ probe_call_count = 0
175
+
176
+ def _counting_probe(**_call_keywords: object) -> _StubQuotaDecision:
177
+ nonlocal probe_call_count
178
+ probe_call_count += 1
179
+ return _StubQuotaDecision(EXIT_CODE_OUT_OF_QUOTA, "down once")
180
+
181
+ monkeypatch.setattr(availability, "evaluate_copilot_quota", _counting_probe)
182
+ availability._probe_copilot_quota()
183
+ availability._probe_copilot_quota()
184
+ assert probe_call_count == 1
185
+
186
+
187
+ def should_resolve_copilot_waiver_from_flag_with_pinned_note() -> None:
188
+ waiver = availability._resolve_copilot_waiver(True)
189
+ assert waiver.is_waived is True
190
+ assert waiver.bypass_note == "copilot_down"
191
+ assert waiver.probe_error_reason == ""
192
+
193
+
194
+ def should_resolve_copilot_waiver_from_frozen_env_with_pinned_note(
195
+ monkeypatch: pytest.MonkeyPatch,
196
+ ) -> None:
197
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "copilot")
198
+ waiver = availability._resolve_copilot_waiver(False)
199
+ assert waiver.is_waived is True
200
+ assert waiver.bypass_note == "copilot_down"
201
+
202
+
203
+ def should_waive_copilot_from_disk_when_env_omits_it_without_union_warning(
204
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, caplog: pytest.LogCaptureFixture
205
+ ) -> None:
206
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugbot")
207
+ settings_path = _write_settings(tmp_path, reviews_disabled_value="bugbot,copilot")
208
+ monkeypatch.setattr(
209
+ availability, "get_claude_user_settings_path", lambda: settings_path
210
+ )
211
+ with caplog.at_level(logging.WARNING):
212
+ waiver = availability._resolve_copilot_waiver(False)
213
+ assert waiver.is_waived is True
214
+ assert waiver.bypass_note == "copilot_down"
215
+ assert "taking the union" not in caplog.text
216
+ assert "differs" not in caplog.text
217
+
218
+
219
+ def should_enforce_copilot_from_disk_when_env_still_lists_it(
220
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
221
+ ) -> None:
222
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "copilot")
223
+ settings_path = _write_settings(tmp_path, reviews_disabled_value="bugbot")
224
+ monkeypatch.setattr(
225
+ availability, "get_claude_user_settings_path", lambda: settings_path
226
+ )
227
+ monkeypatch.setattr(
228
+ availability,
229
+ "evaluate_copilot_quota",
230
+ _stub_quota(EXIT_CODE_QUOTA_AVAILABLE, "quota available"),
231
+ )
232
+ waiver = availability._resolve_copilot_waiver(False)
233
+ assert waiver.is_waived is False
234
+ assert waiver.probe_error_reason == ""
235
+
236
+
237
+ def should_not_fall_through_to_env_when_readable_disk_omits_disabled_key(
238
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
239
+ ) -> None:
240
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "copilot")
241
+ settings_path = _write_settings(tmp_path)
242
+ monkeypatch.setattr(
243
+ availability, "get_claude_user_settings_path", lambda: settings_path
244
+ )
245
+ monkeypatch.setattr(
246
+ availability,
247
+ "evaluate_copilot_quota",
248
+ _stub_quota(EXIT_CODE_QUOTA_AVAILABLE, "quota available"),
249
+ )
250
+ waiver = availability._resolve_copilot_waiver(False)
251
+ assert waiver.is_waived is False
252
+
253
+
254
+ def should_use_env_fallback_and_log_once_when_disk_unreadable(
255
+ monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
256
+ ) -> None:
257
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "copilot,bugbot")
258
+ monkeypatch.setenv("CLAUDE_REVIEWS_ENABLED", "bugbot")
259
+ with caplog.at_level(logging.WARNING):
260
+ copilot_waiver = availability._resolve_copilot_waiver(False)
261
+ bugbot_waiver = availability._resolve_bugbot_waiver(False)
262
+ assert copilot_waiver.is_waived is True
263
+ assert bugbot_waiver.is_waived is True
264
+ fallback_log_count = caplog.text.count("settings.json unreadable")
265
+ assert fallback_log_count == 1
266
+
267
+
268
+ def should_resolve_bugbot_disabled_when_enabled_list_omits_it(
269
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
270
+ ) -> None:
271
+ settings_path = _write_settings(
272
+ tmp_path, reviews_disabled_value="", reviews_enabled_value=""
273
+ )
274
+ monkeypatch.setattr(
275
+ availability, "get_claude_user_settings_path", lambda: settings_path
276
+ )
277
+ waiver = availability._resolve_bugbot_waiver(False)
278
+ assert waiver.is_waived is True
279
+ assert waiver.bypass_note == "bugbot_down"
280
+
281
+
282
+ def should_enforce_bugbot_when_disk_enables_it_even_if_env_omits_opt_in(
283
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
284
+ ) -> None:
285
+ monkeypatch.delenv("CLAUDE_REVIEWS_ENABLED", raising=False)
286
+ settings_path = _write_settings(
287
+ tmp_path, reviews_disabled_value="", reviews_enabled_value="bugbot"
288
+ )
289
+ monkeypatch.setattr(
290
+ availability, "get_claude_user_settings_path", lambda: settings_path
291
+ )
292
+ waiver = availability._resolve_bugbot_waiver(False)
293
+ assert waiver.is_waived is False
294
+ assert waiver.probe_error_reason == ""
295
+
296
+
297
+ def should_read_settings_via_get_claude_user_settings_path(
298
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
299
+ ) -> None:
300
+ settings_path = _write_settings(tmp_path, reviews_disabled_value="copilot")
301
+ resolved_paths: list[Path] = []
302
+
303
+ def _recording_settings_path() -> Path:
304
+ resolved_paths.append(settings_path)
305
+ return settings_path
306
+
307
+ monkeypatch.setattr(
308
+ availability, "get_claude_user_settings_path", _recording_settings_path
309
+ )
310
+ waiver = availability._resolve_copilot_waiver(False)
311
+ assert resolved_paths == [settings_path]
312
+ assert waiver.is_waived is True
313
+ assert waiver.bypass_note == "copilot_down"
314
+
315
+
316
+ def should_probe_copilot_when_neither_flag_env_nor_disk_disable_it(
317
+ monkeypatch: pytest.MonkeyPatch,
318
+ ) -> None:
319
+ monkeypatch.setattr(
320
+ availability,
321
+ "evaluate_copilot_quota",
322
+ _stub_quota(EXIT_CODE_OUT_OF_QUOTA, "quota exhausted"),
323
+ )
324
+ waiver = availability._resolve_copilot_waiver(False)
325
+ assert waiver.is_waived is True
326
+ assert waiver.bypass_note == "copilot unavailable: quota exhausted"