claude-dev-env 1.92.1 → 1.93.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 (214) hide show
  1. package/CLAUDE.md +6 -2
  2. package/_shared/CLAUDE.md +2 -1
  3. package/_shared/advisor/CLAUDE.md +21 -0
  4. package/_shared/advisor/advisor-protocol.md +142 -0
  5. package/_shared/advisor/scripts/config/advisor_scripts_constants/__init__.py +1 -0
  6. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +86 -0
  7. package/_shared/advisor/scripts/model_tier_run_validator.py +246 -0
  8. package/_shared/advisor/scripts/pyproject.toml +3 -0
  9. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +302 -0
  10. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +180 -0
  11. package/_shared/advisor/scripts/tier_model_ids.py +164 -0
  12. package/_shared/pr-loop/CLAUDE.md +1 -0
  13. package/_shared/pr-loop/precatch-rubric.md +65 -0
  14. package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
  15. package/_shared/pr-loop/scripts/code_rules_gate.py +137 -5
  16. package/_shared/pr-loop/scripts/post_audit_thread.py +1 -1
  17. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
  18. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +16 -0
  19. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +2 -1
  20. package/_shared/pr-loop/scripts/reviewer_availability.py +42 -12
  21. package/_shared/pr-loop/scripts/reviews_disabled.py +65 -21
  22. package/_shared/pr-loop/scripts/terminology_sweep.py +69 -21
  23. package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -1
  24. package/_shared/pr-loop/scripts/tests/fixtures/{copilot_internal_user_jonecho.json → copilot_internal_user_example.json} +1 -1
  25. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +1 -1
  26. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +146 -2
  27. package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +8 -8
  28. package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +18 -5
  29. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +56 -5
  30. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +104 -17
  31. package/agents/CLAUDE.md +3 -2
  32. package/agents/clasp-deployment-orchestrator.md +2 -2
  33. package/agents/code-advisor.md +7 -5
  34. package/agents/code-verifier.md +6 -2
  35. package/agents/session-advisor.md +27 -0
  36. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
  37. package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -1
  38. package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
  39. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
  40. package/audit-rubrics/prompts/category-c-resource-cleanup.md +2 -2
  41. package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +2 -2
  42. package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
  43. package/audit-rubrics/prompts/category-f-silent-failures.md +2 -2
  44. package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
  45. package/audit-rubrics/prompts/category-h-security-boundaries.md +2 -2
  46. package/audit-rubrics/prompts/category-i-concurrency.md +2 -2
  47. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +2 -2
  48. package/audit-rubrics/prompts/category-k-codebase-conflicts.md +2 -2
  49. package/audit-rubrics/prompts/category-l-behavior-equivalence.md +2 -2
  50. package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +2 -2
  51. package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +2 -2
  52. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
  53. package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +2 -2
  54. package/bin/CLAUDE.md +1 -0
  55. package/bin/expand_home_directory_tokens.mjs +75 -0
  56. package/bin/install.mjs +21 -7
  57. package/bin/install.test.mjs +220 -10
  58. package/hooks/CLAUDE.md +1 -1
  59. package/hooks/blocking/CLAUDE.md +4 -1
  60. package/hooks/blocking/_path_setup.py +13 -0
  61. package/hooks/blocking/bash_pre_tool_use_dispatcher.py +281 -0
  62. package/hooks/blocking/code_rules_dead_module_constant.py +88 -16
  63. package/hooks/blocking/code_rules_enforcer.py +6 -0
  64. package/hooks/blocking/code_rules_imports_logging.py +34 -1
  65. package/hooks/blocking/code_rules_paths_syspath.py +1 -1
  66. package/hooks/blocking/code_rules_test_layout.py +272 -0
  67. package/hooks/blocking/nas_ssh_binary_enforcer.py +22 -12
  68. package/hooks/blocking/pii_prevention_blocker.py +781 -0
  69. package/hooks/blocking/pii_scanner.py +340 -0
  70. package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
  71. package/hooks/blocking/stop_dispatcher.py +84 -0
  72. package/hooks/blocking/test__path_setup.py +39 -0
  73. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +61 -0
  74. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +294 -0
  75. package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +1 -1
  76. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +51 -0
  77. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +103 -0
  78. package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +1 -1
  79. package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +1 -1
  80. package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +11 -11
  81. package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +1 -1
  82. package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +1 -1
  83. package/hooks/blocking/test_code_rules_enforcer_test_layout.py +111 -0
  84. package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +76 -0
  85. package/hooks/blocking/test_destructive_command_blocker.py +29 -9
  86. package/hooks/blocking/test_gh_body_arg_blocker.py +3 -3
  87. package/hooks/blocking/test_nas_ssh_binary_enforcer.py +96 -39
  88. package/hooks/blocking/test_pii_prevention_blocker.py +905 -0
  89. package/hooks/blocking/test_pii_prevention_windows_git_path.py +41 -0
  90. package/hooks/blocking/test_pii_scanner.py +165 -0
  91. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +1 -1
  92. package/hooks/blocking/test_pre_tool_use_dispatcher.py +14 -13
  93. package/hooks/blocking/test_stop_dispatcher.py +187 -0
  94. package/hooks/blocking/test_verdict_directory_write_blocker.py +1 -1
  95. package/hooks/blocking/test_volatile_path_in_post_blocker.py +3 -3
  96. package/hooks/blocking/volatile_path_in_post_blocker.py +54 -5
  97. package/hooks/diagnostic/migrations/README.md +25 -26
  98. package/hooks/diagnostic/test_hook_log_extractor.py +10 -10
  99. package/hooks/hooks.json +10 -120
  100. package/hooks/hooks_constants/CLAUDE.md +12 -3
  101. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +78 -0
  102. package/hooks/hooks_constants/dead_module_constant_constants.py +1 -0
  103. package/hooks/hooks_constants/hosted_hook_runner.py +73 -0
  104. package/hooks/hooks_constants/local_identity.py +182 -0
  105. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +4 -37
  106. package/hooks/hooks_constants/pii_prevention_constants.py +299 -0
  107. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  108. package/hooks/hooks_constants/python_style_checks_constants.py +15 -0
  109. package/hooks/hooks_constants/stop_dispatcher_constants.py +26 -0
  110. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +87 -0
  111. package/hooks/hooks_constants/test_hardcoded_user_path_constants.py +2 -2
  112. package/hooks/hooks_constants/test_hosted_hook_runner.py +101 -0
  113. package/hooks/hooks_constants/test_layout_constants.py +28 -0
  114. package/hooks/hooks_constants/test_local_identity.py +88 -0
  115. package/hooks/hooks_constants/test_session_env_cleanup_constants.py +1 -1
  116. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +32 -0
  117. package/hooks/validators/README.md +5 -6
  118. package/hooks/validators/__init__.py +2 -2
  119. package/hooks/validators/python_style_checks.py +298 -243
  120. package/hooks/validators/run_all_validators.py +6 -0
  121. package/hooks/validators/test_python_style_checks.py +278 -163
  122. package/package.json +2 -2
  123. package/rules/CLAUDE.md +2 -1
  124. package/rules/bdd.md +1 -1
  125. package/rules/nas-ssh-invocation.md +6 -4
  126. package/rules/no-justification-noise.md +61 -0
  127. package/scripts/test_setup_project_paths.py +1 -1
  128. package/skills/CLAUDE.md +6 -3
  129. package/skills/_shared/pr-loop/scripts/_path_resolver.py +2 -2
  130. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
  131. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
  132. package/skills/_shared/pr-loop/scripts/test__path_resolver.py +42 -3
  133. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +11 -11
  134. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +26 -3
  135. package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +49 -25
  136. package/skills/autoconverge/CLAUDE.md +3 -3
  137. package/skills/autoconverge/SKILL.md +113 -35
  138. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  139. package/skills/autoconverge/reference/convergence.md +44 -22
  140. package/skills/autoconverge/reference/gotchas.md +11 -0
  141. package/skills/autoconverge/reference/stop-conditions.md +23 -13
  142. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  143. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +15 -19
  144. package/skills/autoconverge/workflow/converge.contract.test.mjs +43 -42
  145. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +141 -16
  146. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +2 -2
  147. package/skills/autoconverge/workflow/converge.mjs +329 -68
  148. package/skills/autoconverge/workflow/converge.path-aware.test.mjs +1 -1
  149. package/skills/autoconverge/workflow/converge.precatch.test.mjs +152 -0
  150. package/skills/autoconverge/workflow/converge.run-input.test.mjs +3 -3
  151. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +15 -15
  152. package/skills/autoconverge/workflow/test_convergence_summary.py +6 -6
  153. package/skills/bdd-protocol/SKILL.md +4 -5
  154. package/skills/bdd-protocol/references/anti-patterns.md +1 -1
  155. package/skills/bugteam/SKILL.md +8 -0
  156. package/skills/bugteam/reference/copilot-gap-analysis.md +20 -20
  157. package/skills/bugteam/reference/obstacles/audit-walk-categories.md +1 -1
  158. package/skills/bugteam/reference/team-setup.md +7 -5
  159. package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
  160. package/skills/copilot-finding-triage/SKILL.md +124 -0
  161. package/skills/copilot-finding-triage/reference/tier-rubric.md +49 -0
  162. package/skills/copilot-finding-triage/scripts/conftest.py +8 -0
  163. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/__init__.py +0 -0
  164. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/__init__.py +0 -0
  165. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/notify_ntfy_constants.py +32 -0
  166. package/skills/copilot-finding-triage/scripts/notify_ntfy.py +192 -0
  167. package/skills/copilot-finding-triage/scripts/test_notify_ntfy.py +141 -0
  168. package/skills/copilot-finding-triage/templates/notification.md +34 -0
  169. package/skills/copilot-review/SKILL.md +8 -6
  170. package/skills/findbugs/SKILL.md +4 -0
  171. package/skills/fixbugs/SKILL.md +8 -7
  172. package/skills/gotcha/CLAUDE.md +2 -2
  173. package/skills/gotcha/SKILL.md +4 -4
  174. package/skills/log-audit/SKILL.md +4 -6
  175. package/skills/monitor-open-prs/CLAUDE.md +1 -1
  176. package/skills/monitor-open-prs/SKILL.md +6 -1
  177. package/skills/orchestrator/SKILL.md +74 -78
  178. package/skills/orchestrator-refresh/SKILL.md +24 -12
  179. package/skills/post-audit-findings/SKILL.md +5 -9
  180. package/skills/pr-consistency-audit/SKILL.md +5 -1
  181. package/skills/pr-converge/CLAUDE.md +1 -1
  182. package/skills/pr-converge/SKILL.md +86 -47
  183. package/skills/pr-converge/reference/CLAUDE.md +1 -1
  184. package/skills/pr-converge/reference/convergence-gates.md +20 -17
  185. package/skills/pr-converge/reference/examples.md +63 -47
  186. package/skills/pr-converge/reference/fix-protocol.md +15 -15
  187. package/skills/pr-converge/reference/ground-rules.md +11 -7
  188. package/skills/pr-converge/reference/multi-pr-orchestration.md +6 -5
  189. package/skills/pr-converge/reference/per-tick.md +129 -107
  190. package/skills/pr-converge/reference/state-schema.md +15 -10
  191. package/skills/pr-converge/scripts/check_convergence.py +5 -4
  192. package/skills/pr-converge/scripts/test_check_convergence.py +6 -4
  193. package/skills/pr-fix-protocol/SKILL.md +3 -8
  194. package/skills/pr-loop-cloud-transport/SKILL.md +102 -0
  195. package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +40 -0
  196. package/skills/pr-loop-cloud-transport/reference/substitution-matrix.md +48 -0
  197. package/skills/pr-loop-lifecycle/SKILL.md +5 -9
  198. package/skills/pr-scope-resolve/SKILL.md +3 -6
  199. package/skills/privacy-hygiene/SKILL.md +114 -0
  200. package/skills/qbug/SKILL.md +8 -8
  201. package/skills/rebase/SKILL.md +5 -1
  202. package/skills/refine/SKILL.md +4 -5
  203. package/skills/reviewer-gates/SKILL.md +7 -11
  204. package/skills/session-log/SKILL.md +4 -1
  205. package/skills/skill-builder/SKILL.md +3 -6
  206. package/skills/structure-prompt/SKILL.md +4 -5
  207. package/skills/team-advisor/SKILL.md +56 -0
  208. package/skills/test_markdown_link_integrity.py +10 -6
  209. package/skills/update/SKILL.md +5 -1
  210. package/skills/usage-pause/SKILL.md +14 -5
  211. package/skills/usage-pause/scripts/resolve_usage_window.py +83 -5
  212. package/skills/usage-pause/scripts/test_resolve_usage_window.py +185 -17
  213. package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +4 -3
  214. package/skills/verified-build/SKILL.md +4 -9
@@ -0,0 +1,65 @@
1
+ # Pre-catch lens checklist
2
+
3
+ The shared checklist the internal pre-catch lenses read on demand.
4
+
5
+ ## Lane 1 — Deterministic sweep
6
+
7
+ The mechanical, static, and test classes belong to a deterministic run. The static-sweep
8
+ step owns them: it runs the CODE_RULES gate (`code_rules_gate.py --base origin/main`),
9
+ `ruff`, `mypy`, and stem-matched `pytest` over the changed files. A reading lens trusts the
10
+ sweep to have cleared them and reviews sweep-clean code.
11
+
12
+ - Every CODE_RULES violation the gate reports is a finding.
13
+ - Every `ruff` and `mypy` diagnostic on a changed file is a finding.
14
+ - Every failing test in a changed production module's paired test is a finding.
15
+ - **Proof of absence:** the gate exits 0, `ruff` and `mypy` report no diagnostic
16
+ on the changed files, and the stem-matched tests pass.
17
+
18
+ ## Lane 2 — Doc-vs-code parity
19
+
20
+ Every doc claim in the diff matches the code it describes. Reuse the
21
+ `pr-consistency-audit` skill's canonical-source cross-reference method
22
+ (`~/.claude/skills/pr-consistency-audit/SKILL.md`, canonical source first) and the
23
+ drift rubric at
24
+ `~/.claude/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md`.
25
+
26
+ - Every line citation resolves to the line it names.
27
+ - Every referenced file or script path exists.
28
+ - Every symbol is attributed to the file that defines it.
29
+ - Every inventory table, env-var table, count claim, and ordering claim matches
30
+ the code.
31
+ - **Proof of absence:** each referenced path, citation, symbol, and table checked
32
+ against its source and found in step.
33
+
34
+ ## Lane 3 — Test-assertion completeness
35
+
36
+ Every changed or new production path carries a test that exercises its behavior.
37
+
38
+ - Every changed or new production path has a paired test that calls it and asserts
39
+ on its return value or side effect.
40
+ - A changed test pins the behavior it covers rather than hiding it behind a mock.
41
+ - **Proof of absence:** each changed production path paired with a behavior test
42
+ that fails when the path regresses.
43
+
44
+ ## Lane 4 — PR-description-vs-diff two-way parity
45
+
46
+ The PR body and the diff describe the same change, both directions.
47
+
48
+ - Every claim in the PR body maps to a hunk in the diff.
49
+ - Every hunk in the diff maps to a claim in the PR body.
50
+ - Flag any invented path, invented count, or out-of-scope change.
51
+ - **Proof of absence:** the two-way map complete — every claim has a hunk and
52
+ every hunk has a claim.
53
+
54
+ ## Lane 5 — Adversarial audit
55
+
56
+ The bug-audit lens runs the adversarial second pass the audit contract specifies
57
+ (`~/.claude/_shared/pr-loop/audit-contract.md`): assume the first A-P pass missed
58
+ at least three P1 bugs, then find them.
59
+
60
+ - Return new file:line findings for the bugs the first pass missed.
61
+ - Or return a per-category proof-of-absence entry naming each re-examined category
62
+ and why it holds.
63
+ - A bare "nothing new" is not an acceptable result for this pass.
64
+ - **Proof of absence:** a per-category adversarial-probe entry for each re-examined
65
+ A-P category with no new finding.
@@ -13,7 +13,7 @@ Python scripts invoked at runtime by the PR-loop skills. Each script is a standa
13
13
  | `revoke_project_claude_permissions.py` | Removes the allow-rules and entries that `grant_project_claude_permissions.py` wrote; safe to run when no prior grant exists |
14
14
  | `code_rules_gate.py` | Pre-commit gate that runs `code_rules_enforcer` checks on staged Python files before a fix commit lands, and the terminology sweep over the staged diff |
15
15
  | `terminology_sweep.py` | Flags a prose term that near-misses an identifier introduced on added code lines of a unified diff (shared leading word, divergent tail) |
16
- | `reviews_disabled.py` | Shared helper for the `CLAUDE_REVIEWS_DISABLED` opt-out gate; parses the env-var token to find which reviewer types are suppressed |
16
+ | `reviews_disabled.py` | Shared helper for the reviewer opt-out and opt-in gates; parses `CLAUDE_REVIEWS_DISABLED` and `CLAUDE_REVIEWS_ENABLED` tokens to decide which reviewers run (bugbot is off by default and runs only when `CLAUDE_REVIEWS_ENABLED` lists it) |
17
17
  | `copilot_quota.py` | Copilot premium-request quota pre-check: resolves a configured GitHub account, reads its remaining `premium_interactions` quota via `gh api copilot_internal/user`, and exits 0 (run Copilot) or non-zero (skip: out of quota, API down, or no account configured) |
18
18
  | `reviewer_availability.py` | Unified reviewer-availability entry point for Copilot and Bugbot: reuses `copilot_quota.py` and `reviews_disabled.py` and exits 0 when the named `--reviewer` may be spawned, non-zero when it is opted out or (for Copilot) out of quota |
19
19
  | `fix_hookspath.py` | Repairs a malformed `core.hooksPath` global git config entry |
@@ -1,6 +1,7 @@
1
1
  import argparse
2
2
  import ast
3
3
  import importlib.util
4
+ import os
4
5
  import re
5
6
  import subprocess
6
7
  import sys
@@ -15,13 +16,18 @@ from pr_loop_shared_constants.code_rules_gate_constants import ( # noqa: E402
15
16
  ALL_CODE_FILE_EXTENSIONS,
16
17
  ALL_GIT_DIFF_CACHED_NAME_ONLY_NULL_TERMINATED_COMMAND,
17
18
  ALL_GIT_DIFF_NAME_ONLY_NULL_TERMINATED_COMMAND_PREFIX,
19
+ ALL_POSIX_VENV_PYTHON_RELATIVE_PATH_SEGMENTS,
18
20
  ALL_PYTEST_CONFIG_FILE_SECTIONS,
19
21
  ALL_PYTEST_MODULE_INVOCATION,
20
22
  ALL_TEST_FILENAME_GLOB_SUFFIXES,
21
23
  ALL_TEST_FILENAME_SUFFIXES,
24
+ ALL_VENV_DIRECTORY_NAMES,
25
+ ALL_WINDOWS_VENV_PYTHON_RELATIVE_PATH_SEGMENTS,
22
26
  BANNED_NOUN_DEFINITION_LINE_GROUP_INDEX,
23
27
  BANNED_NOUN_SPAN_GROUP_INDEX,
24
28
  BANNED_NOUN_VIOLATION_PATTERN,
29
+ CODE_RULES_GATE_PYTHON_ENV_VAR,
30
+ CODE_RULES_GATE_PYTHONPATH_ENV_VAR,
25
31
  DUPLICATE_BODY_DEFINITION_LINE_GROUP_INDEX,
26
32
  DUPLICATE_BODY_SPAN_GROUP_INDEX,
27
33
  DUPLICATE_BODY_VIOLATION_PATTERN,
@@ -36,7 +42,10 @@ from pr_loop_shared_constants.code_rules_gate_constants import ( # noqa: E402
36
42
  ISOLATION_SPAN_GROUP_INDEX,
37
43
  ISOLATION_VIOLATION_PATTERN,
38
44
  MAX_VIOLATIONS_PER_CHECK,
45
+ MINIMUM_STAGED_PYTEST_PYTHON_MAJOR,
46
+ MINIMUM_STAGED_PYTEST_PYTHON_MINOR,
39
47
  PYTHON_FILE_EXTENSION,
48
+ PYTHONPATH_ENV_VAR,
40
49
  STAGED_PYTEST_TIMEOUT_SECONDS,
41
50
  STAGED_TEST_FAILURE_HEADER,
42
51
  STAGED_TEST_GROUP_FAILURE_MESSAGE,
@@ -1620,12 +1629,97 @@ def _group_staged_tests_by_root(
1620
1629
  return tests_by_root
1621
1630
 
1622
1631
 
1623
- def _run_pytest_for_group(group_root: Path, all_group_test_paths: list[Path]) -> int:
1632
+ def _venv_python_relative_path() -> tuple[str, ...]:
1633
+ """Return the venv-relative python executable path segments for this platform.
1634
+
1635
+ Returns:
1636
+ The Windows ``Scripts/python.exe`` segments on Windows, the POSIX
1637
+ ``bin/python`` segments on every other platform.
1638
+ """
1639
+ if os.name == "nt":
1640
+ return ALL_WINDOWS_VENV_PYTHON_RELATIVE_PATH_SEGMENTS
1641
+ return ALL_POSIX_VENV_PYTHON_RELATIVE_PATH_SEGMENTS
1642
+
1643
+
1644
+ def _venv_python_executable(repository_root: Path) -> Path | None:
1645
+ """Return the first existing venv interpreter under *repository_root*.
1646
+
1647
+ Checks each directory name in ``ALL_VENV_DIRECTORY_NAMES`` in order and
1648
+ returns the platform-specific interpreter path the first time it exists
1649
+ on disk.
1650
+
1651
+ Args:
1652
+ repository_root: The repository root to search for a venv directory.
1653
+
1654
+ Returns:
1655
+ The resolved interpreter path, or None when no candidate exists.
1656
+ """
1657
+ relative_python_path = _venv_python_relative_path()
1658
+ for each_venv_directory_name in ALL_VENV_DIRECTORY_NAMES:
1659
+ candidate_path = repository_root.joinpath(
1660
+ each_venv_directory_name, *relative_python_path
1661
+ )
1662
+ if candidate_path.is_file():
1663
+ return candidate_path
1664
+ return None
1665
+
1666
+
1667
+ def _resolve_gate_python_executable(repository_root: Path) -> str:
1668
+ """Return the interpreter the staged-test subprocess runs under.
1669
+
1670
+ Resolves in order: the ``CODE_RULES_GATE_PYTHON`` environment variable
1671
+ when set, then the first existing project venv interpreter under
1672
+ *repository_root*, then the interpreter running the gate itself.
1673
+
1674
+ Args:
1675
+ repository_root: The repository root used to locate a project venv.
1676
+
1677
+ Returns:
1678
+ The absolute path (or command name) of the interpreter to invoke.
1679
+ """
1680
+ configured_python = os.environ.get(CODE_RULES_GATE_PYTHON_ENV_VAR)
1681
+ if configured_python:
1682
+ return configured_python
1683
+ venv_python = _venv_python_executable(repository_root)
1684
+ if venv_python is not None:
1685
+ return str(venv_python)
1686
+ return sys.executable
1687
+
1688
+
1689
+ def _staged_pytest_environment() -> dict[str, str]:
1690
+ """Return the subprocess environment for the staged-test pytest run.
1691
+
1692
+ Starts from ``repository_environment()`` (the GIT_-scrubbed process
1693
+ environment) and, when ``CODE_RULES_GATE_PYTHONPATH`` is set, prepends
1694
+ its value to ``PYTHONPATH`` so a resolved venv interpreter that lacks the
1695
+ project on its default path can still import it.
1696
+
1697
+ Returns:
1698
+ The environment mapping to pass to the staged-test subprocess.
1699
+ """
1700
+ environment = repository_environment()
1701
+ configured_pythonpath = os.environ.get(CODE_RULES_GATE_PYTHONPATH_ENV_VAR)
1702
+ if not configured_pythonpath:
1703
+ return environment
1704
+ existing_pythonpath = environment.get(PYTHONPATH_ENV_VAR, "")
1705
+ environment[PYTHONPATH_ENV_VAR] = (
1706
+ configured_pythonpath
1707
+ if not existing_pythonpath
1708
+ else os.pathsep.join([configured_pythonpath, existing_pythonpath])
1709
+ )
1710
+ return environment
1711
+
1712
+
1713
+ def _run_pytest_for_group(
1714
+ group_root: Path, all_group_test_paths: list[Path], repository_root: Path
1715
+ ) -> int:
1624
1716
  """Run pytest over one group's test files with the working directory at its root.
1625
1717
 
1626
1718
  Args:
1627
1719
  group_root: The owning test root used as the pytest working directory.
1628
1720
  all_group_test_paths: The staged test files that share *group_root*.
1721
+ repository_root: The repository root used to resolve a project venv
1722
+ interpreter when ``CODE_RULES_GATE_PYTHON`` is not set.
1629
1723
 
1630
1724
  Returns:
1631
1725
  0 when the group's tests pass or collect nothing; pytest's non-zero
@@ -1633,14 +1727,14 @@ def _run_pytest_for_group(group_root: Path, all_group_test_paths: list[Path]) ->
1633
1727
  """
1634
1728
  pytest_process = subprocess.run(
1635
1729
  [
1636
- sys.executable,
1730
+ _resolve_gate_python_executable(repository_root),
1637
1731
  *ALL_PYTEST_MODULE_INVOCATION,
1638
1732
  *[str(each_path) for each_path in all_group_test_paths],
1639
1733
  ],
1640
1734
  cwd=str(group_root),
1641
1735
  timeout=STAGED_PYTEST_TIMEOUT_SECONDS,
1642
1736
  check=False,
1643
- env=repository_environment(),
1737
+ env=_staged_pytest_environment(),
1644
1738
  )
1645
1739
  if pytest_process.returncode == PYTEST_NO_TESTS_COLLECTED_EXIT_CODE:
1646
1740
  return 0
@@ -1670,7 +1764,7 @@ def run_staged_test_files(repository_root: Path) -> int:
1670
1764
  first_failing_exit_code = 0
1671
1765
  for each_group_root in sorted(tests_by_root):
1672
1766
  group_exit_code = _run_pytest_for_group(
1673
- each_group_root, tests_by_root[each_group_root]
1767
+ each_group_root, tests_by_root[each_group_root], repository_root
1674
1768
  )
1675
1769
  if group_exit_code != 0:
1676
1770
  print(
@@ -1684,6 +1778,42 @@ def run_staged_test_files(repository_root: Path) -> int:
1684
1778
  return first_failing_exit_code
1685
1779
 
1686
1780
 
1781
+ def _staged_pytest_exit_code_for_current_python(repository_root: Path) -> int:
1782
+ """Run the staged test files, or skip them on a Python below the minimum.
1783
+
1784
+ ::
1785
+
1786
+ running (3, 11), minimum (3, 12) -> log a notice and return 0 (skipped)
1787
+ running (3, 12), minimum (3, 12) -> run pytest over the staged tests
1788
+
1789
+ The staged tests call newer-Python APIs, so an older interpreter fails them
1790
+ for the environment rather than for a code defect. On such a runtime this
1791
+ step is skipped so the gap does not block the commit, while the terminology
1792
+ and code-rules checks still run.
1793
+
1794
+ Args:
1795
+ repository_root: The repository root whose staged test files run.
1796
+
1797
+ Returns:
1798
+ 0 when the running Python is below the staged-test minimum, otherwise the
1799
+ exit code from running the staged test files.
1800
+ """
1801
+ running_version = (sys.version_info.major, sys.version_info.minor)
1802
+ minimum_version = (
1803
+ MINIMUM_STAGED_PYTEST_PYTHON_MAJOR,
1804
+ MINIMUM_STAGED_PYTEST_PYTHON_MINOR,
1805
+ )
1806
+ if running_version < minimum_version:
1807
+ print(
1808
+ f"code_rules_gate: Python {running_version} is below the staged-test "
1809
+ f"minimum {minimum_version}; skipping the staged "
1810
+ "pytest step (environment gap, not a code failure).",
1811
+ file=sys.stderr,
1812
+ )
1813
+ return 0
1814
+ return run_staged_test_files(repository_root)
1815
+
1816
+
1687
1817
  def _report_terminology_findings(all_findings: list[str]) -> None:
1688
1818
  """Print the terminology-sweep findings, when any, to standard error.
1689
1819
 
@@ -1782,7 +1912,9 @@ def main(all_arguments: list[str]) -> int:
1782
1912
  if arguments.staged:
1783
1913
  all_terminology_findings = staged_terminology_findings(repository_root)
1784
1914
  _report_terminology_findings(all_terminology_findings)
1785
- staged_test_exit_code = run_staged_test_files(repository_root)
1915
+ staged_test_exit_code = _staged_pytest_exit_code_for_current_python(
1916
+ repository_root
1917
+ )
1786
1918
  staged_file_paths = paths_from_git_staged(repository_root)
1787
1919
  staged_file_paths = filter_paths_under_prefixes(
1788
1920
  staged_file_paths,
@@ -223,7 +223,7 @@ def parse_command_line_arguments(all_arguments: list[str]) -> argparse.Namespace
223
223
  parser.add_argument(
224
224
  CLI_FLAG_REPO,
225
225
  required=True,
226
- help="Repository name (e.g., claude-code-config).",
226
+ help="Repository name (e.g., claude-dev-env).",
227
227
  )
228
228
  parser.add_argument(
229
229
  CLI_FLAG_PR_NUMBER,
@@ -14,7 +14,7 @@ Named constants for every script in `_shared/pr-loop/scripts/`. Each module owns
14
14
  | `post_audit_thread_constants.py` | `post_audit_thread.py` — HTTP status codes, retry counts, GitHub API paths |
15
15
  | `preflight_constants.py` | `preflight.py` — env-var names, git subcommands, pytest exit codes, test discovery patterns |
16
16
  | `preflight_self_heal_constants.py` | `preflight_self_heal.py` — git config keys and local-scope detection strings |
17
- | `reviews_disabled_constants.py` | `reviews_disabled.py` — `CLAUDE_REVIEWS_DISABLED` token taxonomy |
17
+ | `reviews_disabled_constants.py` | `reviews_disabled.py` — `CLAUDE_REVIEWS_DISABLED` and `CLAUDE_REVIEWS_ENABLED` env-var names and token taxonomy |
18
18
  | `copilot_quota_constants.py` | `copilot_quota.py` — `COPILOT_QUOTA_ACCOUNT` env-var name, `copilot_internal/user` API path, the `premium_interactions` gating field names, the four skip/run exit codes, and the default `.env` path |
19
19
  | `terminology_sweep_constants.py` | `terminology_sweep.py` — identifier and prose-token regexes, diff-parsing prefixes, code-file extensions, test-file detection patterns, and the finding-message template |
20
20
  | `reviewer_availability_constants.py` | `reviewer_availability.py` — the available/down exit codes for the unified reviewer-availability pre-check |
@@ -88,6 +88,18 @@ ALL_PYTEST_MODULE_INVOCATION: tuple[str, ...] = (
88
88
  "-q",
89
89
  )
90
90
 
91
+ CODE_RULES_GATE_PYTHON_ENV_VAR: str = "CODE_RULES_GATE_PYTHON"
92
+
93
+ CODE_RULES_GATE_PYTHONPATH_ENV_VAR: str = "CODE_RULES_GATE_PYTHONPATH"
94
+
95
+ PYTHONPATH_ENV_VAR: str = "PYTHONPATH"
96
+
97
+ ALL_VENV_DIRECTORY_NAMES: tuple[str, ...] = (".venv", "venv")
98
+
99
+ ALL_WINDOWS_VENV_PYTHON_RELATIVE_PATH_SEGMENTS: tuple[str, ...] = ("Scripts", "python.exe")
100
+
101
+ ALL_POSIX_VENV_PYTHON_RELATIVE_PATH_SEGMENTS: tuple[str, ...] = ("bin", "python")
102
+
91
103
  STAGED_PYTEST_TIMEOUT_SECONDS: int = 600
92
104
 
93
105
  STAGED_TEST_FAILURE_HEADER: str = (
@@ -119,3 +131,7 @@ STAGED_TEST_GROUP_FAILURE_MESSAGE: str = (
119
131
  "code_rules_gate: staged test group rooted at {group_root} "
120
132
  "failed under pytest; commit blocked."
121
133
  )
134
+
135
+ MINIMUM_STAGED_PYTEST_PYTHON_MAJOR: int = 3
136
+
137
+ MINIMUM_STAGED_PYTEST_PYTHON_MINOR: int = 12
@@ -1,8 +1,9 @@
1
- """Configuration constants for the CLAUDE_REVIEWS_DISABLED opt-out gate."""
1
+ """Configuration constants for the reviewer opt-out and opt-in gates."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
5
  CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME: str = "CLAUDE_REVIEWS_DISABLED"
6
+ CLAUDE_REVIEWS_ENABLED_ENV_VAR_NAME: str = "CLAUDE_REVIEWS_ENABLED"
6
7
  CLAUDE_REVIEWS_DISABLED_TOKEN_SEPARATOR: str = ","
7
8
  CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN: str = "bugteam"
8
9
  CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN: str = "bugbot"
@@ -11,9 +11,11 @@ Copilot counts as down in two cases: it is opted out via
11
11
  than quota available (out of quota, the quota API or account down, or no
12
12
  account configured).
13
13
 
14
- Bugbot carries no quota or availability API. It counts as down only when it
15
- is opted out via ``CLAUDE_REVIEWS_DISABLED``. A genuine runtime outage shows
16
- up later as a poll timeout, not here.
14
+ Bugbot carries no quota or availability API. It is off by default and counts
15
+ as down whenever it is disabled for the run: the default unless
16
+ ``CLAUDE_REVIEWS_ENABLED`` lists ``bugbot``, and always when
17
+ ``CLAUDE_REVIEWS_DISABLED`` lists it. A genuine runtime outage shows up later
18
+ as a poll timeout, not here.
17
19
 
18
20
  The exit code tells the caller what to do. Exit 0 means the reviewer is
19
21
  available and may be spawned. The documented down code (3) means skip it. Any
@@ -42,8 +44,13 @@ from pr_loop_shared_constants.reviews_disabled_constants import (
42
44
  CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN,
43
45
  CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN,
44
46
  CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME,
47
+ CLAUDE_REVIEWS_ENABLED_ENV_VAR_NAME,
48
+ )
49
+ from reviews_disabled import (
50
+ is_bugbot_disabled_via_env,
51
+ is_bugbot_opted_out_via_env,
52
+ is_copilot_disabled_via_env,
45
53
  )
46
- from reviews_disabled import is_bugbot_disabled_via_env, is_copilot_disabled_via_env
47
54
 
48
55
 
49
56
  @dataclass(frozen=True)
@@ -84,22 +91,45 @@ def _evaluate_copilot_availability(env_file_path: Path) -> ReviewerAvailability:
84
91
  return ReviewerAvailability(EXIT_CODE_REVIEWER_AVAILABLE, quota_decision.message)
85
92
 
86
93
 
94
+ def _bugbot_down_message() -> str:
95
+ """Name the cause that keeps Cursor Bugbot off for this run.
96
+
97
+ An opt-out and the off-by-default state read differently to the operator:
98
+ one names ``CLAUDE_REVIEWS_DISABLED``, the other points at the
99
+ ``CLAUDE_REVIEWS_ENABLED`` opt-in the run is missing.
100
+
101
+ Returns:
102
+ The opt-out message when ``CLAUDE_REVIEWS_DISABLED`` lists ``bugbot``,
103
+ and the off-by-default opt-in message otherwise.
104
+ """
105
+ if is_bugbot_opted_out_via_env():
106
+ return (
107
+ f"reviewer-availability: bugbot is disabled via "
108
+ f"{CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME} — skipping."
109
+ )
110
+ return (
111
+ f"reviewer-availability: bugbot is off by default — set "
112
+ f"{CLAUDE_REVIEWS_ENABLED_ENV_VAR_NAME}={CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN} "
113
+ f"to opt in — skipping."
114
+ )
115
+
116
+
87
117
  def _evaluate_bugbot_availability() -> ReviewerAvailability:
88
118
  """Decide whether Cursor Bugbot is available to spawn.
89
119
 
90
120
  Bugbot carries no quota or availability API, so this checks only the
91
- deterministic ``CLAUDE_REVIEWS_DISABLED`` opt-out.
121
+ deterministic enable/disable env gate. Bugbot is off by default: it is
122
+ available only when ``CLAUDE_REVIEWS_ENABLED`` lists ``bugbot`` and
123
+ ``CLAUDE_REVIEWS_DISABLED`` does not.
92
124
 
93
125
  Returns:
94
- A ReviewerAvailability that is down when Bugbot is opted out via
95
- ``CLAUDE_REVIEWS_DISABLED``, and available otherwise.
126
+ A ReviewerAvailability that is down whenever Bugbot is disabled for
127
+ the run the default unless ``CLAUDE_REVIEWS_ENABLED`` lists
128
+ ``bugbot``, and always when ``CLAUDE_REVIEWS_DISABLED`` lists it — and
129
+ available otherwise.
96
130
  """
97
131
  if is_bugbot_disabled_via_env():
98
- return ReviewerAvailability(
99
- EXIT_CODE_REVIEWER_DOWN,
100
- f"reviewer-availability: bugbot is disabled via "
101
- f"{CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME} — skipping.",
102
- )
132
+ return ReviewerAvailability(EXIT_CODE_REVIEWER_DOWN, _bugbot_down_message())
103
133
  return ReviewerAvailability(
104
134
  EXIT_CODE_REVIEWER_AVAILABLE,
105
135
  "reviewer-availability: bugbot is available.",
@@ -1,8 +1,16 @@
1
- """Shared helper for the CLAUDE_REVIEWS_DISABLED opt-out gate.
1
+ """Decide which PR reviewers run from the opt-out and opt-in env token lists.
2
2
 
3
- Both ``skills/bugteam/scripts/bugteam_preflight.py`` and
4
- ``_shared/pr-loop/scripts/preflight.py`` consume this helper so the parsing
5
- rules and disabled-token taxonomy live in exactly one place.
3
+ ::
4
+
5
+ enabled lists bugbot, disabled empty -> bugbot ok: runs
6
+ no lists set -> bugbot flag: off (default)
7
+ enabled lists bugbot, disabled bugbot -> bugbot flag: off (opt-out wins)
8
+ disabled empty -> bugteam ok: runs
9
+ disabled lists copilot -> copilot flag: off
10
+
11
+ Bugbot is off by default and runs only when the enabled list names it.
12
+ Bugteam and copilot run by default and stop only when the disabled list
13
+ names them; both lists parse case-insensitively and tolerate whitespace.
6
14
  """
7
15
 
8
16
  from __future__ import annotations
@@ -17,6 +25,7 @@ from pr_loop_shared_constants.reviews_disabled_constants import (
17
25
  CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN,
18
26
  CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME,
19
27
  CLAUDE_REVIEWS_DISABLED_TOKEN_SEPARATOR,
28
+ CLAUDE_REVIEWS_ENABLED_ENV_VAR_NAME,
20
29
  EXIT_CODE_BUGTEAM_DISABLED_VIA_ENV,
21
30
  )
22
31
 
@@ -27,18 +36,24 @@ __all__ = [
27
36
  "CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN",
28
37
  "CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME",
29
38
  "CLAUDE_REVIEWS_DISABLED_TOKEN_SEPARATOR",
39
+ "CLAUDE_REVIEWS_ENABLED_ENV_VAR_NAME",
30
40
  "EXIT_CODE_BUGTEAM_DISABLED_VIA_ENV",
31
41
  "is_bugbot_disabled_via_env",
42
+ "is_bugbot_opted_out_via_env",
32
43
  "is_bugteam_disabled_via_env",
33
44
  "is_copilot_disabled_via_env",
34
45
  "main",
35
46
  ]
36
47
 
37
48
 
38
- def _is_reviewer_disabled_via_env(reviewer_token: str) -> bool:
39
- """Check whether CLAUDE_REVIEWS_DISABLED lists the given reviewer token.
49
+ def _is_reviewer_listed_in_env(
50
+ environment_variable_name: str, reviewer_token: str
51
+ ) -> bool:
52
+ """Check whether an environment variable lists the given reviewer token.
40
53
 
41
54
  Args:
55
+ environment_variable_name: The environment variable to read, either
56
+ the reviews-disabled or the reviews-enabled variable name.
42
57
  reviewer_token: The reviewer token to look for, already lowercase
43
58
  (for example the bugteam or bugbot token constant).
44
59
 
@@ -46,16 +61,14 @@ def _is_reviewer_disabled_via_env(reviewer_token: str) -> bool:
46
61
  True when the env var contains ``reviewer_token`` as one of its
47
62
  comma-separated entries (case-insensitive, whitespace-tolerant).
48
63
  """
49
- reviews_disabled_token_separator = CLAUDE_REVIEWS_DISABLED_TOKEN_SEPARATOR
50
- disabled_reviewers_text = os.environ.get(CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME, "")
51
- all_disabled_tokens = frozenset(
64
+ reviews_token_separator = CLAUDE_REVIEWS_DISABLED_TOKEN_SEPARATOR
65
+ listed_reviewers_text = os.environ.get(environment_variable_name, "")
66
+ all_listed_tokens = frozenset(
52
67
  each_raw_token.strip().lower()
53
- for each_raw_token in disabled_reviewers_text.split(
54
- reviews_disabled_token_separator
55
- )
68
+ for each_raw_token in listed_reviewers_text.split(reviews_token_separator)
56
69
  if each_raw_token.strip()
57
70
  )
58
- return reviewer_token in all_disabled_tokens
71
+ return reviewer_token in all_listed_tokens
59
72
 
60
73
 
61
74
  def is_bugteam_disabled_via_env() -> bool:
@@ -64,16 +77,45 @@ def is_bugteam_disabled_via_env() -> bool:
64
77
  Returns:
65
78
  True when the env var lists the ``bugteam`` token.
66
79
  """
67
- return _is_reviewer_disabled_via_env(CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN)
80
+ return _is_reviewer_listed_in_env(
81
+ CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME, CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN
82
+ )
68
83
 
69
84
 
70
85
  def is_bugbot_disabled_via_env() -> bool:
86
+ """Check whether Cursor Bugbot is disabled for this run.
87
+
88
+ Cursor Bugbot is off by default. It runs only when
89
+ ``CLAUDE_REVIEWS_ENABLED`` lists ``bugbot``, and a ``bugbot`` token in
90
+ ``CLAUDE_REVIEWS_DISABLED`` forces it off even when the opt-in lists it.
91
+
92
+ Returns:
93
+ True when ``CLAUDE_REVIEWS_DISABLED`` lists ``bugbot`` or when
94
+ ``CLAUDE_REVIEWS_ENABLED`` does not list ``bugbot``.
95
+ """
96
+ is_opted_out = _is_reviewer_listed_in_env(
97
+ CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME, CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN
98
+ )
99
+ is_opted_in = _is_reviewer_listed_in_env(
100
+ CLAUDE_REVIEWS_ENABLED_ENV_VAR_NAME, CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN
101
+ )
102
+ return is_opted_out or not is_opted_in
103
+
104
+
105
+ def is_bugbot_opted_out_via_env() -> bool:
71
106
  """Check whether CLAUDE_REVIEWS_DISABLED opts Cursor Bugbot out.
72
107
 
108
+ The opt-out forces Bugbot off even when ``CLAUDE_REVIEWS_ENABLED`` lists
109
+ ``bugbot``. This reports only the opt-out signal, where
110
+ ``is_bugbot_disabled_via_env`` also reports off for the default case in
111
+ which neither env var names ``bugbot``.
112
+
73
113
  Returns:
74
- True when the env var lists the ``bugbot`` token.
114
+ True when ``CLAUDE_REVIEWS_DISABLED`` lists the ``bugbot`` token.
75
115
  """
76
- return _is_reviewer_disabled_via_env(CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN)
116
+ return _is_reviewer_listed_in_env(
117
+ CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME, CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN
118
+ )
77
119
 
78
120
 
79
121
  def is_copilot_disabled_via_env() -> bool:
@@ -82,11 +124,13 @@ def is_copilot_disabled_via_env() -> bool:
82
124
  Returns:
83
125
  True when the env var lists the ``copilot`` token.
84
126
  """
85
- return _is_reviewer_disabled_via_env(CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN)
127
+ return _is_reviewer_listed_in_env(
128
+ CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME, CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN
129
+ )
86
130
 
87
131
 
88
132
  def parse_arguments(all_argv: list[str]) -> argparse.Namespace:
89
- """Parse command-line arguments for the reviewer opt-out check.
133
+ """Parse command-line arguments for the reviewer opt-out and opt-in gate check.
90
134
 
91
135
  Args:
92
136
  all_argv: Argument list excluding the program name, typically
@@ -105,19 +149,19 @@ def parse_arguments(all_argv: list[str]) -> argparse.Namespace:
105
149
  CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN,
106
150
  CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN,
107
151
  ],
108
- help="Reviewer token to test against CLAUDE_REVIEWS_DISABLED",
152
+ help="Reviewer token to test against the CLAUDE_REVIEWS_DISABLED / CLAUDE_REVIEWS_ENABLED gates",
109
153
  )
110
154
  return parser.parse_args(all_argv)
111
155
 
112
156
 
113
157
  def main(all_arguments: list[str]) -> int:
114
- """Exit 0 when the named reviewer is disabled via CLAUDE_REVIEWS_DISABLED.
158
+ """Exit 0 when the named reviewer is disabled for this run.
115
159
 
116
160
  Args:
117
161
  all_arguments: Argument list excluding the program name.
118
162
 
119
163
  Returns:
120
- 0 when the named reviewer is opted out by the env var, 1 otherwise.
164
+ 0 when the named reviewer is disabled for this run, 1 otherwise.
121
165
  """
122
166
  arguments = parse_arguments(all_arguments)
123
167
  disabled_checker_by_reviewer = {