claude-dev-env 1.45.0 → 1.46.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.
@@ -5,4 +5,5 @@ from __future__ import annotations
5
5
  CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME: str = "CLAUDE_REVIEWS_DISABLED"
6
6
  CLAUDE_REVIEWS_DISABLED_TOKEN_SEPARATOR: str = ","
7
7
  CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN: str = "bugteam"
8
+ CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN: str = "bugbot"
8
9
  EXIT_CODE_BUGTEAM_DISABLED_VIA_ENV: int = 7
@@ -7,9 +7,12 @@ rules and disabled-token taxonomy live in exactly one place.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import argparse
10
11
  import os
12
+ import sys
11
13
 
12
14
  from pr_loop_shared_constants.reviews_disabled_constants import (
15
+ CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN,
13
16
  CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN,
14
17
  CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME,
15
18
  CLAUDE_REVIEWS_DISABLED_TOKEN_SEPARATOR,
@@ -18,28 +21,98 @@ from pr_loop_shared_constants.reviews_disabled_constants import (
18
21
 
19
22
 
20
23
  __all__ = [
24
+ "CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN",
21
25
  "CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN",
22
26
  "CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME",
23
27
  "CLAUDE_REVIEWS_DISABLED_TOKEN_SEPARATOR",
24
28
  "EXIT_CODE_BUGTEAM_DISABLED_VIA_ENV",
29
+ "is_bugbot_disabled_via_env",
25
30
  "is_bugteam_disabled_via_env",
31
+ "main",
26
32
  ]
27
33
 
28
34
 
29
- def is_bugteam_disabled_via_env() -> bool:
30
- """Check whether CLAUDE_REVIEWS_DISABLED opts the bug-audit family out of running.
35
+ def _is_reviewer_disabled_via_env(reviewer_token: str) -> bool:
36
+ """Check whether CLAUDE_REVIEWS_DISABLED lists the given reviewer token.
37
+
38
+ Args:
39
+ reviewer_token: The reviewer token to look for, already lowercase
40
+ (for example the bugteam or bugbot token constant).
31
41
 
32
42
  Returns:
33
- True when the env var contains the literal ``bugteam`` token
34
- (comma-separated, case-insensitive, whitespace-tolerant).
43
+ True when the env var contains ``reviewer_token`` as one of its
44
+ comma-separated entries (case-insensitive, whitespace-tolerant).
35
45
  """
36
- reviews_disabled_env_var_name = CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME
37
46
  reviews_disabled_token_separator = CLAUDE_REVIEWS_DISABLED_TOKEN_SEPARATOR
38
- reviews_disabled_bugteam_token = CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN
39
- raw_value = os.environ.get(reviews_disabled_env_var_name, "")
47
+ disabled_reviewers_text = os.environ.get(CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME, "")
40
48
  all_disabled_tokens = frozenset(
41
49
  each_raw_token.strip().lower()
42
- for each_raw_token in raw_value.split(reviews_disabled_token_separator)
50
+ for each_raw_token in disabled_reviewers_text.split(
51
+ reviews_disabled_token_separator
52
+ )
43
53
  if each_raw_token.strip()
44
54
  )
45
- return reviews_disabled_bugteam_token in all_disabled_tokens
55
+ return reviewer_token in all_disabled_tokens
56
+
57
+
58
+ def is_bugteam_disabled_via_env() -> bool:
59
+ """Check whether CLAUDE_REVIEWS_DISABLED opts the bug-audit family out.
60
+
61
+ Returns:
62
+ True when the env var lists the ``bugteam`` token.
63
+ """
64
+ return _is_reviewer_disabled_via_env(CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN)
65
+
66
+
67
+ def is_bugbot_disabled_via_env() -> bool:
68
+ """Check whether CLAUDE_REVIEWS_DISABLED opts Cursor Bugbot out.
69
+
70
+ Returns:
71
+ True when the env var lists the ``bugbot`` token.
72
+ """
73
+ return _is_reviewer_disabled_via_env(CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN)
74
+
75
+
76
+ def parse_arguments(all_argv: list[str]) -> argparse.Namespace:
77
+ """Parse command-line arguments for the reviewer opt-out check.
78
+
79
+ Args:
80
+ all_argv: Argument list excluding the program name, typically
81
+ ``sys.argv[1:]``.
82
+
83
+ Returns:
84
+ Namespace exposing a ``reviewer`` attribute constrained to the
85
+ known reviewer tokens.
86
+ """
87
+ parser = argparse.ArgumentParser(description=__doc__)
88
+ parser.add_argument(
89
+ "--reviewer",
90
+ required=True,
91
+ choices=[
92
+ CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN,
93
+ CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN,
94
+ ],
95
+ help="Reviewer token to test against CLAUDE_REVIEWS_DISABLED",
96
+ )
97
+ return parser.parse_args(all_argv)
98
+
99
+
100
+ def main(all_arguments: list[str]) -> int:
101
+ """Exit 0 when the named reviewer is disabled via CLAUDE_REVIEWS_DISABLED.
102
+
103
+ Args:
104
+ all_arguments: Argument list excluding the program name.
105
+
106
+ Returns:
107
+ 0 when the named reviewer is opted out by the env var, 1 otherwise.
108
+ """
109
+ arguments = parse_arguments(all_arguments)
110
+ disabled_checker_by_reviewer = {
111
+ CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN: is_bugbot_disabled_via_env,
112
+ CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN: is_bugteam_disabled_via_env,
113
+ }
114
+ return 0 if disabled_checker_by_reviewer[arguments.reviewer]() else 1
115
+
116
+
117
+ if __name__ == "__main__":
118
+ raise SystemExit(main(sys.argv[1:]))
@@ -49,6 +49,11 @@ def initialize_git_repository(repository_root: Path) -> None:
49
49
  run_git_in_repository(repository_root, "config", "user.email", "test@example.com")
50
50
  run_git_in_repository(repository_root, "config", "user.name", "Test")
51
51
  run_git_in_repository(repository_root, "config", "commit.gpgsign", "false")
52
+ disabled_hooks_directory = repository_root / "disabled-git-hooks"
53
+ disabled_hooks_directory.mkdir()
54
+ run_git_in_repository(
55
+ repository_root, "config", "core.hooksPath", str(disabled_hooks_directory)
56
+ )
52
57
 
53
58
 
54
59
  def commit_all_files(repository_root: Path, commit_message: str) -> None:
@@ -34,3 +34,60 @@ def test_is_bugteam_disabled_via_env_returns_false_when_env_is_empty(
34
34
  ) -> None:
35
35
  monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
36
36
  assert reviews_disabled.is_bugteam_disabled_via_env() is False
37
+
38
+
39
+ def test_is_bugbot_disabled_via_env_returns_true_when_env_lists_bugbot(
40
+ monkeypatch: pytest.MonkeyPatch,
41
+ ) -> None:
42
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugbot")
43
+ assert reviews_disabled.is_bugbot_disabled_via_env() is True
44
+
45
+
46
+ def test_is_bugbot_disabled_via_env_returns_false_when_env_is_empty(
47
+ monkeypatch: pytest.MonkeyPatch,
48
+ ) -> None:
49
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
50
+ assert reviews_disabled.is_bugbot_disabled_via_env() is False
51
+
52
+
53
+ def test_is_bugbot_disabled_via_env_returns_false_when_only_bugteam_listed(
54
+ monkeypatch: pytest.MonkeyPatch,
55
+ ) -> None:
56
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugteam")
57
+ assert reviews_disabled.is_bugbot_disabled_via_env() is False
58
+
59
+
60
+ def test_is_bugbot_disabled_via_env_true_when_both_tokens_listed_mixed_case(
61
+ monkeypatch: pytest.MonkeyPatch,
62
+ ) -> None:
63
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", " BugTeam , BUGBOT ")
64
+ assert reviews_disabled.is_bugbot_disabled_via_env() is True
65
+ assert reviews_disabled.is_bugteam_disabled_via_env() is True
66
+
67
+
68
+ def test_cli_main_returns_zero_when_named_reviewer_disabled(
69
+ monkeypatch: pytest.MonkeyPatch,
70
+ ) -> None:
71
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugbot")
72
+ assert reviews_disabled.main(["--reviewer", "bugbot"]) == 0
73
+
74
+
75
+ def test_cli_main_returns_one_when_named_reviewer_not_disabled(
76
+ monkeypatch: pytest.MonkeyPatch,
77
+ ) -> None:
78
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
79
+ assert reviews_disabled.main(["--reviewer", "bugbot"]) == 1
80
+
81
+
82
+ def test_cli_main_returns_one_when_other_reviewer_disabled(
83
+ monkeypatch: pytest.MonkeyPatch,
84
+ ) -> None:
85
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugteam")
86
+ assert reviews_disabled.main(["--reviewer", "bugbot"]) == 1
87
+
88
+
89
+ def test_cli_main_supports_bugteam_reviewer(
90
+ monkeypatch: pytest.MonkeyPatch,
91
+ ) -> None:
92
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugteam")
93
+ assert reviews_disabled.main(["--reviewer", "bugteam"]) == 0
@@ -7,12 +7,31 @@ def get_zoekt_redirect_reason_brief() -> str:
7
7
  )
8
8
 
9
9
 
10
+ def worktree_path_filter_fragment() -> str:
11
+ return "\\.claude/worktrees/"
12
+
13
+
14
+ def worktree_path_display_fragment() -> str:
15
+ return ".claude/worktrees/"
16
+
17
+
10
18
  def get_zoekt_redirect_guidance() -> str:
19
+ worktree_filter_fragment = worktree_path_filter_fragment()
20
+ worktree_display_fragment = worktree_path_display_fragment()
11
21
  return (
12
22
  "Use Zoekt MCP instead: mcp__zoekt__search(query=\"your pattern\"). "
13
23
  "Supports regex, 'file:pattern' for file filtering, 'lang:py' for language. "
14
24
  "Also available: mcp__zoekt__search_symbols, mcp__zoekt__find_references, mcp__zoekt__file_content. "
15
25
  "Example: mcp__zoekt__search(query=\"verify_theme_assets file:\\.py$\")\n\n"
26
+ "WORKTREE NOISE: indexed trees include git worktrees under "
27
+ + worktree_display_fragment
28
+ + " that duplicate the same code across branches. By default append '-file:"
29
+ + worktree_filter_fragment
30
+ + "' to each query so results stay on the primary checkout. When the user explicitly "
31
+ "asks about a worktree, branch, or PR checkout, drop that exclusion or filter to it "
32
+ "positively, e.g. mcp__zoekt__search(query=\"your pattern file:"
33
+ + worktree_filter_fragment
34
+ + "<branch>/\").\n\n"
16
35
  "INDEX ROOTS (when Grep/Search in a tree is redirected): set ZOEKT_REDIRECT_INDEXED_ROOTS to a JSON array "
17
36
  "of absolute paths, or ~/.claude/zoekt-indexed-roots.json as {\"roots\": [\"/abs/path/to/repo/\", ...]}. "
18
37
  "Optional ZOEKT_REDIRECT_INDEXED_ROOTS_FILE points to a different JSON file. "
@@ -14,6 +14,8 @@ from content_search_zoekt_block_payload import build_block_payload
14
14
  from content_search_zoekt_redirect_guidance import (
15
15
  get_zoekt_redirect_guidance,
16
16
  get_zoekt_redirect_reason_brief,
17
+ worktree_path_display_fragment,
18
+ worktree_path_filter_fragment,
17
19
  )
18
20
 
19
21
 
@@ -52,5 +54,33 @@ class BuildBlockPayloadTests(unittest.TestCase):
52
54
  )
53
55
 
54
56
 
57
+ class RedirectGuidanceWorktreeTests(unittest.TestCase):
58
+ def test_guidance_defaults_to_excluding_worktrees(self) -> None:
59
+ guidance = get_zoekt_redirect_guidance()
60
+ expected_default_exclusion = f"-file:{worktree_path_filter_fragment()}"
61
+ self.assertIn(expected_default_exclusion, guidance)
62
+
63
+ def test_guidance_explains_how_to_search_a_worktree(self) -> None:
64
+ guidance = get_zoekt_redirect_guidance()
65
+ positive_filter_example = (
66
+ f'query="your pattern file:{worktree_path_filter_fragment()}<branch>/'
67
+ )
68
+ self.assertIn(positive_filter_example, guidance)
69
+ self.assertIn("worktree", guidance.lower())
70
+
71
+ def test_worktree_filter_fragment_is_a_regex_escaped_path(self) -> None:
72
+ self.assertEqual(worktree_path_filter_fragment(), "\\.claude/worktrees/")
73
+
74
+ def test_guidance_describes_worktree_path_unescaped_in_prose(self) -> None:
75
+ guidance = get_zoekt_redirect_guidance()
76
+ prose_substring = (
77
+ f"git worktrees under {worktree_path_display_fragment()} that"
78
+ )
79
+ self.assertIn(prose_substring, guidance)
80
+
81
+ def test_worktree_display_fragment_is_the_unescaped_path(self) -> None:
82
+ self.assertEqual(worktree_path_display_fragment(), ".claude/worktrees/")
83
+
84
+
55
85
  if __name__ == "__main__":
56
86
  unittest.main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.45.0",
3
+ "version": "1.46.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,6 +33,11 @@ def initialize_git_repository(repository_root: Path) -> None:
33
33
  run_git_in_repository(repository_root, "config", "user.email", "test@example.com")
34
34
  run_git_in_repository(repository_root, "config", "user.name", "Test")
35
35
  run_git_in_repository(repository_root, "config", "commit.gpgsign", "false")
36
+ disabled_hooks_directory = repository_root / "disabled-git-hooks"
37
+ disabled_hooks_directory.mkdir()
38
+ run_git_in_repository(
39
+ repository_root, "config", "core.hooksPath", str(disabled_hooks_directory)
40
+ )
36
41
 
37
42
 
38
43
  def commit_all_files(repository_root: Path, commit_message: str) -> None:
@@ -125,6 +125,11 @@ no longer applies.
125
125
  - [ ] **Step 4: BUGBOT — fetch, decide, fix, reply, resolve**
126
126
  See: [`reference/per-tick.md` § Step 2 BUGBOT + Step 3](reference/per-tick.md)
127
127
 
128
+ - [ ] **Opt-out gate (runs first, every BUGBOT entry).**
129
+ `python "$HOME/.claude/_shared/pr-loop/scripts/reviews_disabled.py" --reviewer bugbot`
130
+ - [ ] Exit 0 (`CLAUDE_REVIEWS_DISABLED` lists `bugbot`) → set `bugbot_down = true`, `phase = BUGTEAM`, advance to Step 5 (bypass). Cursor Bugbot is skipped for the entire run.
131
+ - [ ] Exit 1 → continue below.
132
+
128
133
  Fetch bugbot reviews + inline comments on `current_head`.
129
134
 
130
135
  - [ ] **dirty** (findings on `current_head`) →
@@ -49,6 +49,16 @@ Capture `number`, `head.sha` (= `current_head`), owner/repo, branch.
49
49
 
50
50
  ### `phase == BUGBOT`
51
51
 
52
+ **Opt-out gate (runs first, before any fetch or trigger).**
53
+ `python "$HOME/.claude/_shared/pr-loop/scripts/reviews_disabled.py" --reviewer bugbot`
54
+
55
+ - Exit 0 (`CLAUDE_REVIEWS_DISABLED` lists `bugbot`) → set `bugbot_down = true`,
56
+ `phase = BUGTEAM`, continue BUGTEAM in the same tick; skip steps a–c below.
57
+ - Exit 1 → proceed to step a.
58
+
59
+ Because `bugbot_down` resets on every push, this gate re-runs on every
60
+ BUGBOT entry and keeps Cursor Bugbot skipped for the entire run.
61
+
52
62
  a. Fetch Cursor Bugbot reviews newest-first, walk back until first clean:
53
63
 
54
64
  ```
@@ -207,11 +217,10 @@ BUGBOT.
207
217
 
208
218
  ## Step 3: Re-trigger bugbot
209
219
 
210
- - [ ] **Opt-out gate.** When `CLAUDE_REVIEWS_DISABLED` (comma-separated,
211
- case-insensitive, whitespace-tolerant) contains `bugbot`, set
212
- `bugbot_down = true`, skip every check below, set `phase = BUGTEAM`,
213
- and continue BUGTEAM in the same tick. The downstream loop branches on
214
- `bugbot_down` exactly the way it does when bugbot CI is unavailable.
220
+ - [ ] **Opt-out gate.** Enforced at BUGBOT entry (see `### phase == BUGBOT`).
221
+ When `CLAUDE_REVIEWS_DISABLED` lists `bugbot`, the entry gate sets
222
+ `bugbot_down = true` and routes to BUGTEAM before any trigger flow runs,
223
+ so the checks below are skipped.
215
224
  - [ ] **Silent-pass pre-check.** Run `python ~/.claude/skills/pr-converge/scripts/check_bugbot_ci.py --check-clean --owner <O> --repo <R> --sha <current_head>`
216
225
  - [ ] Exit 0 → bugbot CI completed clean with no review (silent pass); set `bugbot_clean_at = current_head`, `phase = BUGTEAM`, continue BUGTEAM same tick
217
226
  - [ ] Exit 1 (not a silent pass) or Exit 2 (gh CLI error — silent pass not confirmable) → continue with the trigger flow below
@@ -23,9 +23,13 @@ live ONLY in the single-PR `$CLAUDE_JOB_DIR/pr-converge-state.json` file
23
23
  matching. Reset to `0` on any other branch outcome.
24
24
  - `bugbot_down`: boolean, init `false`. Set `true` when bugbot fails to
25
25
  acknowledge a trigger comment; forces phase to BUGTEAM. Also set `true`
26
- when an acknowledged trigger has been outstanding more than 30 minutes
27
- with no surfaced review at `current_head` (per Step 2 BUGBOT (c)
28
- 30-minute budget see `per-tick.md`). Reset to `false` on every push.
26
+ at every BUGBOT-phase entry when `CLAUDE_REVIEWS_DISABLED` lists the
27
+ `bugbot` token (env opt-out via the BUGBOT entry gate in `per-tick.md`),
28
+ which routes straight to BUGTEAM before any bugbot fetch or trigger. Also
29
+ set `true` when an acknowledged trigger has been outstanding more than 30
30
+ minutes with no surfaced review at `current_head` (per Step 2 BUGBOT (c)
31
+ 30-minute budget — see `per-tick.md`). Reset to `false` on every push;
32
+ the entry gate re-applies the env opt-out on the next BUGBOT entry.
29
33
  Once set, remains `true` until the next push; if bugbot stays down
30
34
  across ticks, the flag persists and BUGTEAM continues.
31
35
  - `bugbot_acknowledged_at`: ISO 8601 timestamp string or `null`. Records
@@ -4,6 +4,10 @@ Usage:
4
4
  python scripts/check_convergence.py --owner <O> --repo <R> --pr-number <N>
5
5
  [--bugbot-down]
6
6
 
7
+ The bugbot check-run gate is bypassed when either ``--bugbot-down`` is
8
+ passed OR the ``CLAUDE_REVIEWS_DISABLED`` environment variable lists the
9
+ ``bugbot`` token, so a Bugbot opt-out closes the gate without the flag.
10
+
7
11
  Exit codes:
8
12
  0 — all pre-conditions met
9
13
  1 — one or more conditions not met (FAIL lines printed to stdout)
@@ -48,6 +52,14 @@ from pr_converge_skill_constants.constants import (
48
52
  UNRESOLVED_THREAD_DETAIL_MAX,
49
53
  )
50
54
 
55
+ _shared_pr_loop_scripts_dir = (
56
+ Path(__file__).absolute().parents[3] / "_shared" / "pr-loop" / "scripts"
57
+ )
58
+ if str(_shared_pr_loop_scripts_dir) not in sys.path:
59
+ sys.path.insert(0, str(_shared_pr_loop_scripts_dir))
60
+
61
+ from reviews_disabled import is_bugbot_disabled_via_env
62
+
51
63
 
52
64
  def _is_bugteam_review(review_body: str) -> bool:
53
65
  """Return True when a review body opens with a bugteam audit header.
@@ -621,6 +633,20 @@ def parse_arguments(all_argv: list[str]) -> argparse.Namespace:
621
633
  return parser.parse_args(all_argv)
622
634
 
623
635
 
636
+ def _resolve_bugbot_down(bugbot_down_flag: bool) -> bool:
637
+ """Combine the explicit flag with the CLAUDE_REVIEWS_DISABLED env opt-out.
638
+
639
+ Args:
640
+ bugbot_down_flag: Value of the ``--bugbot-down`` CLI flag.
641
+
642
+ Returns:
643
+ True when the flag is set OR ``CLAUDE_REVIEWS_DISABLED`` lists the
644
+ ``bugbot`` token, so the env opt-out bypasses the bugbot gates even
645
+ when the caller omits the flag.
646
+ """
647
+ return bugbot_down_flag or is_bugbot_disabled_via_env()
648
+
649
+
624
650
  def main(all_arguments: list[str]) -> int:
625
651
  """Run the script end-to-end against parsed CLI arguments.
626
652
 
@@ -635,7 +661,7 @@ def main(all_arguments: list[str]) -> int:
635
661
  owner=arguments.owner,
636
662
  repo=arguments.repo,
637
663
  number=getattr(arguments, "pr_number"),
638
- bugbot_down=arguments.bugbot_down,
664
+ bugbot_down=_resolve_bugbot_down(arguments.bugbot_down),
639
665
  )
640
666
 
641
667
 
@@ -217,6 +217,34 @@ def test_private_helpers_recognize_dirty_legacy_header_body() -> None:
217
217
  assert check_convergence._is_clean_bugteam_review(DIRTY_LEGACY_BUGTEAM_BODY) is False
218
218
 
219
219
 
220
+ def should_resolve_bugbot_down_true_when_flag_set(
221
+ monkeypatch: pytest.MonkeyPatch,
222
+ ) -> None:
223
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
224
+ assert check_convergence._resolve_bugbot_down(True) is True
225
+
226
+
227
+ def should_resolve_bugbot_down_true_when_env_disables_bugbot(
228
+ monkeypatch: pytest.MonkeyPatch,
229
+ ) -> None:
230
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugbot")
231
+ assert check_convergence._resolve_bugbot_down(False) is True
232
+
233
+
234
+ def should_resolve_bugbot_down_false_when_flag_unset_and_env_empty(
235
+ monkeypatch: pytest.MonkeyPatch,
236
+ ) -> None:
237
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
238
+ assert check_convergence._resolve_bugbot_down(False) is False
239
+
240
+
241
+ def should_resolve_bugbot_down_false_when_env_disables_only_bugteam(
242
+ monkeypatch: pytest.MonkeyPatch,
243
+ ) -> None:
244
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugteam")
245
+ assert check_convergence._resolve_bugbot_down(False) is False
246
+
247
+
220
248
  def should_bypass_bugbot_gates_when_bugbot_down_is_true(
221
249
  monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
222
250
  ) -> None: