claude-dev-env 1.39.0 → 1.41.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 (60) hide show
  1. package/CLAUDE.md +1 -1
  2. package/_shared/pr-loop/scripts/config/post_audit_thread_constants.py +10 -0
  3. package/_shared/pr-loop/scripts/config/reviews_disabled_constants.py +8 -0
  4. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +53 -3
  5. package/_shared/pr-loop/scripts/post_audit_thread.py +298 -3
  6. package/_shared/pr-loop/scripts/preflight.py +129 -2
  7. package/_shared/pr-loop/scripts/reviews_disabled.py +59 -0
  8. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +68 -3
  9. package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +1 -1
  10. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +194 -1
  11. package/_shared/pr-loop/scripts/tests/test_preflight.py +41 -0
  12. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +36 -0
  13. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
  14. package/agents/pr-description-writer.md +150 -52
  15. package/docs/PR_DESCRIPTION_GUIDE.md +127 -64
  16. package/hooks/_gh_pr_author_swap_utils.py +1211 -0
  17. package/hooks/blocking/gh_body_arg_blocker.py +9 -6
  18. package/hooks/blocking/gh_pr_author_enforcer.py +480 -0
  19. package/hooks/blocking/gh_pr_author_restore.py +100 -0
  20. package/hooks/blocking/pr_converge_bugteam_enforcer.py +170 -0
  21. package/hooks/blocking/pr_description_enforcer.py +56 -23
  22. package/hooks/blocking/test_gh_body_arg_blocker.py +25 -3
  23. package/hooks/blocking/test_gh_pr_author_enforcer.py +1166 -0
  24. package/hooks/blocking/test_gh_pr_author_restore.py +512 -0
  25. package/hooks/blocking/test_gh_pr_author_swap_utils.py +910 -0
  26. package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +311 -0
  27. package/hooks/blocking/test_pr_description_enforcer.py +69 -8
  28. package/hooks/config/gh_pr_author_swap_constants.py +76 -0
  29. package/hooks/config/pr_converge_bugteam_enforcer_constants.py +55 -0
  30. package/hooks/config/pr_converge_bugteam_enforcer_state.py +67 -0
  31. package/hooks/config/pr_description_enforcer_constants.py +19 -0
  32. package/hooks/config/test_pr_description_enforcer_constants.py +82 -0
  33. package/hooks/hooks.json +40 -0
  34. package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +204 -0
  35. package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +283 -0
  36. package/hooks/session/gh_pr_author_session_cleanup.py +171 -0
  37. package/hooks/session/test_gh_pr_author_session_cleanup.py +575 -0
  38. package/hooks/test__gh_pr_author_swap_utils.py +333 -0
  39. package/package.json +1 -1
  40. package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +2 -2
  41. package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +2 -2
  42. package/skills/bugteam/SKILL.md +28 -10
  43. package/skills/bugteam/reference/audit-contract.md +22 -0
  44. package/skills/bugteam/reference/github-pr-reviews.md +1 -1
  45. package/skills/bugteam/reference/team-setup.md +5 -0
  46. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +8 -2
  47. package/skills/bugteam/scripts/bugteam_preflight.py +36 -2
  48. package/skills/bugteam/scripts/test__claude_permissions_common.py +48 -0
  49. package/skills/bugteam/scripts/test_bugteam_preflight.py +41 -0
  50. package/skills/bugteam/scripts/test_claude_permissions_common.py +18 -10
  51. package/skills/copilot-review/SKILL.md +16 -0
  52. package/skills/findbugs/SKILL.md +35 -7
  53. package/skills/monitor-open-prs/SKILL.md +2 -1
  54. package/skills/pr-converge/SKILL.md +11 -3
  55. package/skills/pr-converge/config/constants.py +3 -1
  56. package/skills/pr-converge/reference/per-tick.md +17 -0
  57. package/skills/pr-converge/reference/state-schema.md +36 -8
  58. package/skills/pr-converge/scripts/check_bugbot_ci.py +113 -8
  59. package/skills/pr-converge/scripts/test_check_bugbot_ci.py +312 -0
  60. package/skills/qbug/SKILL.md +33 -8
@@ -0,0 +1,312 @@
1
+ """Tests for check_bugbot_ci silent-pass detection.
2
+
3
+ Covers:
4
+ - is_bugbot_run_clean returns True for completed success / completed neutral
5
+ - is_bugbot_run_clean returns False for completed failure, in_progress, missing
6
+ - is_bugbot_run_clean returns None when the gh CLI fails
7
+ - main(--check-clean) returns 0 on clean, 1 on not-clean, and
8
+ EXIT_CODE_GH_ERROR on gh CLI failure (with stderr diagnostics)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import importlib.util
14
+ import json
15
+ import subprocess
16
+ import sys
17
+ from collections.abc import Iterator
18
+ from pathlib import Path
19
+ from types import ModuleType
20
+ from unittest.mock import MagicMock, patch
21
+
22
+ import pytest
23
+
24
+ _SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
25
+
26
+
27
+ @pytest.fixture(scope="session")
28
+ def check_bugbot_ci_module() -> Iterator[ModuleType]:
29
+ """Load check_bugbot_ci with full sys.path and sys.modules isolation.
30
+
31
+ Snapshots sys.path and sys.modules at session start; restores both
32
+ unconditionally at session teardown so sibling tests inherit a clean
33
+ loading environment. The production script performs its own
34
+ membership-guarded sys.path.insert during exec_module so its config
35
+ dependency resolves; that insert and any config.* modules it loads
36
+ are reverted on teardown.
37
+ """
38
+ module_path = _SCRIPTS_DIRECTORY / "check_bugbot_ci.py"
39
+ spec = importlib.util.spec_from_file_location("check_bugbot_ci", module_path)
40
+ assert spec is not None
41
+ assert spec.loader is not None
42
+ module = importlib.util.module_from_spec(spec)
43
+ sys_path_snapshot = list(sys.path)
44
+ sys_modules_snapshot = dict(sys.modules)
45
+ evicted_config_modules = {
46
+ each_module_name: sys.modules.pop(each_module_name)
47
+ for each_module_name in list(sys.modules)
48
+ if each_module_name == "config" or each_module_name.startswith("config.")
49
+ }
50
+ sys_modules_snapshot.update(evicted_config_modules)
51
+ spec.loader.exec_module(module)
52
+ try:
53
+ yield module
54
+ finally:
55
+ sys.path[:] = sys_path_snapshot
56
+ for each_new_module_name in list(sys.modules):
57
+ if each_new_module_name not in sys_modules_snapshot:
58
+ del sys.modules[each_new_module_name]
59
+ for each_module_name, each_module_value in sys_modules_snapshot.items():
60
+ sys.modules[each_module_name] = each_module_value
61
+
62
+
63
+ def _make_completed_process(
64
+ stdout: str, returncode: int = 0
65
+ ) -> subprocess.CompletedProcess[str]:
66
+ process = MagicMock(spec=subprocess.CompletedProcess)
67
+ process.stdout = stdout
68
+ process.stderr = ""
69
+ process.returncode = returncode
70
+ return process
71
+
72
+
73
+ def _build_stdout(*all_check_entries: dict[str, object]) -> str:
74
+ return "\n".join(json.dumps(each_entry) for each_entry in all_check_entries) + "\n"
75
+
76
+
77
+ def test_should_return_true_when_bugbot_completed_with_success_conclusion(
78
+ check_bugbot_ci_module: ModuleType,
79
+ ) -> None:
80
+ stdout = _build_stdout(
81
+ {"name": "Cursor Bugbot", "status": "completed", "conclusion": "success"}
82
+ )
83
+ with patch.object(
84
+ check_bugbot_ci_module,
85
+ "_run_check_runs_api",
86
+ return_value=_make_completed_process(stdout),
87
+ ):
88
+ is_clean = check_bugbot_ci_module.is_bugbot_run_clean(
89
+ owner="acme", repo="repo", sha="abc"
90
+ )
91
+ assert is_clean is True
92
+
93
+
94
+ def test_should_return_true_when_bugbot_completed_with_neutral_conclusion(
95
+ check_bugbot_ci_module: ModuleType,
96
+ ) -> None:
97
+ stdout = _build_stdout(
98
+ {"name": "bugbot", "status": "completed", "conclusion": "neutral"}
99
+ )
100
+ with patch.object(
101
+ check_bugbot_ci_module,
102
+ "_run_check_runs_api",
103
+ return_value=_make_completed_process(stdout),
104
+ ):
105
+ is_clean = check_bugbot_ci_module.is_bugbot_run_clean(
106
+ owner="acme", repo="repo", sha="abc"
107
+ )
108
+ assert is_clean is True
109
+
110
+
111
+ def test_should_return_false_when_bugbot_completed_with_failure_conclusion(
112
+ check_bugbot_ci_module: ModuleType,
113
+ ) -> None:
114
+ stdout = _build_stdout(
115
+ {"name": "Cursor Bugbot", "status": "completed", "conclusion": "failure"}
116
+ )
117
+ with patch.object(
118
+ check_bugbot_ci_module,
119
+ "_run_check_runs_api",
120
+ return_value=_make_completed_process(stdout),
121
+ ):
122
+ is_clean = check_bugbot_ci_module.is_bugbot_run_clean(
123
+ owner="acme", repo="repo", sha="abc"
124
+ )
125
+ assert is_clean is False
126
+
127
+
128
+ def test_should_return_false_when_bugbot_still_in_progress(
129
+ check_bugbot_ci_module: ModuleType,
130
+ ) -> None:
131
+ stdout = _build_stdout(
132
+ {"name": "Cursor Bugbot", "status": "in_progress", "conclusion": None}
133
+ )
134
+ with patch.object(
135
+ check_bugbot_ci_module,
136
+ "_run_check_runs_api",
137
+ return_value=_make_completed_process(stdout),
138
+ ):
139
+ is_clean = check_bugbot_ci_module.is_bugbot_run_clean(
140
+ owner="acme", repo="repo", sha="abc"
141
+ )
142
+ assert is_clean is False
143
+
144
+
145
+ def test_should_return_false_when_no_bugbot_check_run_present(
146
+ check_bugbot_ci_module: ModuleType,
147
+ ) -> None:
148
+ stdout = _build_stdout(
149
+ {"name": "ci-other", "status": "completed", "conclusion": "success"}
150
+ )
151
+ with patch.object(
152
+ check_bugbot_ci_module,
153
+ "_run_check_runs_api",
154
+ return_value=_make_completed_process(stdout),
155
+ ):
156
+ is_clean = check_bugbot_ci_module.is_bugbot_run_clean(
157
+ owner="acme", repo="repo", sha="abc"
158
+ )
159
+ assert is_clean is False
160
+
161
+
162
+ def test_should_return_false_when_first_bugbot_run_is_in_progress_even_if_later_one_clean(
163
+ check_bugbot_ci_module: ModuleType,
164
+ ) -> None:
165
+ stdout = _build_stdout(
166
+ {"name": "Cursor Bugbot", "status": "in_progress", "conclusion": None},
167
+ {"name": "Cursor Bugbot", "status": "completed", "conclusion": "success"},
168
+ )
169
+ with patch.object(
170
+ check_bugbot_ci_module,
171
+ "_run_check_runs_api",
172
+ return_value=_make_completed_process(stdout),
173
+ ):
174
+ is_clean = check_bugbot_ci_module.is_bugbot_run_clean(
175
+ owner="acme", repo="repo", sha="abc"
176
+ )
177
+ assert is_clean is False
178
+
179
+
180
+ def test_should_return_false_when_first_bugbot_run_failed_even_if_later_one_clean(
181
+ check_bugbot_ci_module: ModuleType,
182
+ ) -> None:
183
+ stdout = _build_stdout(
184
+ {"name": "Cursor Bugbot", "status": "completed", "conclusion": "failure"},
185
+ {"name": "Cursor Bugbot", "status": "completed", "conclusion": "success"},
186
+ )
187
+ with patch.object(
188
+ check_bugbot_ci_module,
189
+ "_run_check_runs_api",
190
+ return_value=_make_completed_process(stdout),
191
+ ):
192
+ is_clean = check_bugbot_ci_module.is_bugbot_run_clean(
193
+ owner="acme", repo="repo", sha="abc"
194
+ )
195
+ assert is_clean is False
196
+
197
+
198
+ def test_should_return_none_when_gh_cli_fails(
199
+ check_bugbot_ci_module: ModuleType,
200
+ ) -> None:
201
+ failing_process = MagicMock(spec=subprocess.CompletedProcess)
202
+ failing_process.stdout = ""
203
+ failing_process.stderr = "boom"
204
+ failing_process.returncode = 1
205
+ with patch.object(
206
+ check_bugbot_ci_module,
207
+ "_run_check_runs_api",
208
+ return_value=failing_process,
209
+ ):
210
+ is_clean = check_bugbot_ci_module.is_bugbot_run_clean(
211
+ owner="acme", repo="repo", sha="abc"
212
+ )
213
+ assert is_clean is None
214
+
215
+
216
+ def test_main_check_clean_should_return_gh_error_code_when_gh_cli_fails(
217
+ check_bugbot_ci_module: ModuleType,
218
+ capsys: pytest.CaptureFixture[str],
219
+ ) -> None:
220
+ failing_process = MagicMock(spec=subprocess.CompletedProcess)
221
+ failing_process.stdout = ""
222
+ failing_process.stderr = "boom"
223
+ failing_process.returncode = 1
224
+ with patch.object(
225
+ check_bugbot_ci_module,
226
+ "_run_check_runs_api",
227
+ return_value=failing_process,
228
+ ):
229
+ exit_code = check_bugbot_ci_module.main(
230
+ ["--check-clean", "--owner", "acme", "--repo", "repo", "--sha", "abc"]
231
+ )
232
+ assert exit_code == check_bugbot_ci_module.EXIT_CODE_GH_ERROR
233
+ captured = capsys.readouterr()
234
+ assert "gh api error: boom" in captured.err
235
+
236
+
237
+ def test_main_check_clean_should_return_zero_when_bugbot_clean(
238
+ check_bugbot_ci_module: ModuleType,
239
+ capsys: pytest.CaptureFixture[str],
240
+ ) -> None:
241
+ stdout = _build_stdout(
242
+ {"name": "Cursor Bugbot", "status": "completed", "conclusion": "success"}
243
+ )
244
+ with patch.object(
245
+ check_bugbot_ci_module,
246
+ "_run_check_runs_api",
247
+ return_value=_make_completed_process(stdout),
248
+ ):
249
+ exit_code = check_bugbot_ci_module.main(
250
+ ["--check-clean", "--owner", "acme", "--repo", "repo", "--sha", "abc"]
251
+ )
252
+ assert exit_code == 0
253
+ captured = capsys.readouterr()
254
+ assert "not clean" not in captured.out
255
+
256
+
257
+ def test_main_check_clean_should_return_one_when_bugbot_not_clean(
258
+ check_bugbot_ci_module: ModuleType,
259
+ capsys: pytest.CaptureFixture[str],
260
+ ) -> None:
261
+ stdout = _build_stdout(
262
+ {"name": "Cursor Bugbot", "status": "completed", "conclusion": "failure"}
263
+ )
264
+ with patch.object(
265
+ check_bugbot_ci_module,
266
+ "_run_check_runs_api",
267
+ return_value=_make_completed_process(stdout),
268
+ ):
269
+ exit_code = check_bugbot_ci_module.main(
270
+ ["--check-clean", "--owner", "acme", "--repo", "repo", "--sha", "abc"]
271
+ )
272
+ assert exit_code == 1
273
+ captured = capsys.readouterr()
274
+ assert "not clean" in captured.out
275
+
276
+
277
+ def test_main_check_active_should_return_zero_when_bugbot_in_progress(
278
+ check_bugbot_ci_module: ModuleType,
279
+ ) -> None:
280
+ stdout = _build_stdout(
281
+ {"name": "Cursor Bugbot", "status": "in_progress", "conclusion": None}
282
+ )
283
+ with patch.object(
284
+ check_bugbot_ci_module,
285
+ "_run_check_runs_api",
286
+ return_value=_make_completed_process(stdout),
287
+ ):
288
+ exit_code = check_bugbot_ci_module.main(
289
+ ["--check-active", "--owner", "acme", "--repo", "repo", "--sha", "abc"]
290
+ )
291
+ assert exit_code == 0
292
+
293
+
294
+ def test_main_should_reject_check_clean_and_check_active_together(
295
+ check_bugbot_ci_module: ModuleType,
296
+ capsys: pytest.CaptureFixture[str],
297
+ ) -> None:
298
+ with pytest.raises(SystemExit):
299
+ check_bugbot_ci_module.main(
300
+ [
301
+ "--check-clean",
302
+ "--check-active",
303
+ "--owner",
304
+ "acme",
305
+ "--repo",
306
+ "repo",
307
+ "--sha",
308
+ "abc",
309
+ ]
310
+ )
311
+ captured = capsys.readouterr()
312
+ assert "not allowed with" in captured.err or "mutually exclusive" in captured.err
@@ -31,6 +31,12 @@ Shared artifacts with /bugteam are referenced below by path, using the `${CLAUDE
31
31
 
32
32
  Refusals — first match wins; respond with the quoted line exactly and stop:
33
33
 
34
+ - **Disabled via environment.** When `CLAUDE_REVIEWS_DISABLED` contains the
35
+ token `bugteam` (comma-separated, case-insensitive, whitespace-tolerant):
36
+ `/qbug is disabled via CLAUDE_REVIEWS_DISABLED.` `/qbug` is the bugteam
37
+ baseline review and shares the `bugteam` token with `/bugteam`; the shared
38
+ pre-flight script also exits 7 in this case so any caller invoking it
39
+ directly halts on the same signal.
34
40
  - **No PR or upstream diff.** `No PR or upstream diff. /qbug needs a target.`
35
41
  - **Dirty tree.** `Uncommitted changes detected. Stash, commit, or revert before /qbug.`
36
42
  - **Missing subagent.** Before Step 2, confirm `clean-coder` exists. Else: `Required subagent type clean-coder not installed. /qbug needs clean-coder available.`
@@ -222,14 +228,33 @@ The subagent receives this prompt and loops internally — the lead does not re-
222
228
  `[]` and pass `--state CLEAN`; one or more anchored findings →
223
229
  pass `--state DIRTY` with the full list.
224
230
 
225
- **Self-PR precondition.** GitHub rejects both `APPROVE` and
226
- `REQUEST_CHANGES` reviews when the authenticated identity matches
227
- the PR author with HTTP 422; `post_audit_thread.py` retries and
228
- then exits 2. To run qbug on a PR you authored, switch `gh auth`
229
- to an alternate reviewer identity (a separate GitHub account)
230
- BEFORE invoking the skill. Without this switch, exit 2 is a hard
231
- haltthere is no automated fallback path. The script does not
232
- auto-downgrade on the self-PR case.
231
+ **Self-PR auto-toggle.** GitHub rejects both `APPROVE` and
232
+ `REQUEST_CHANGES` reviews with HTTP 422 when the authenticated
233
+ identity matches the PR author ("Cannot approve/request changes
234
+ on your own pull request"). `post_audit_thread.py` detects this
235
+ case via `gh api user` + `gh api repos/<o>/<r>/pulls/<n>` and
236
+ auto-resolves an alternate gh account's token for the reviews
237
+ POSTthe active `gh auth` account is not mutated; only the
238
+ bearer token sent on the request changes. After the POST the
239
+ active account is still whoever it was before, so no "swap back"
240
+ step is needed.
241
+
242
+ Configuration:
243
+
244
+ - `GH_TOKEN` / `GITHUB_TOKEN` env vars take precedence over the
245
+ toggle. Set them when you need to pin a specific reviewer
246
+ identity by token rather than by account login.
247
+ - `BUGTEAM_REVIEWER_ACCOUNT` env var names which authenticated
248
+ alternate to prefer when a toggle is needed (for example,
249
+ `BUGTEAM_REVIEWER_ACCOUNT=jl-cmd`). The env var name is shared
250
+ across every skill that invokes `post_audit_thread.py`. When
251
+ unset, the script falls back to the first alternate account
252
+ `gh auth status` reports.
253
+ - The named alternate must be logged in (`gh auth login -h
254
+ github.com -u <login>`) before the audit skill runs. The
255
+ script exits 1 with a pointing-at-`gh auth login` message
256
+ when self-PR is detected and no usable alternate is
257
+ authenticated.
233
258
 
234
259
  ```
235
260
  python "${CLAUDE_SKILL_DIR}/../../_shared/pr-loop/scripts/post_audit_thread.py" \