claude-dev-env 1.39.0 → 1.40.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.
- package/_shared/pr-loop/scripts/config/post_audit_thread_constants.py +10 -0
- package/_shared/pr-loop/scripts/config/reviews_disabled_constants.py +8 -0
- package/_shared/pr-loop/scripts/post_audit_thread.py +296 -1
- package/_shared/pr-loop/scripts/preflight.py +129 -2
- package/_shared/pr-loop/scripts/reviews_disabled.py +59 -0
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +194 -1
- package/_shared/pr-loop/scripts/tests/test_preflight.py +41 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +36 -0
- package/agents/pr-description-writer.md +150 -52
- package/docs/PR_DESCRIPTION_GUIDE.md +127 -64
- package/hooks/blocking/pr_description_enforcer.py +57 -22
- package/hooks/blocking/test_pr_description_enforcer.py +69 -8
- package/hooks/config/pr_description_enforcer_constants.py +14 -0
- package/package.json +1 -1
- package/skills/bugteam/SKILL.md +28 -10
- package/skills/bugteam/reference/team-setup.md +5 -0
- package/skills/bugteam/scripts/bugteam_preflight.py +36 -2
- package/skills/bugteam/scripts/test_bugteam_preflight.py +41 -0
- package/skills/copilot-review/SKILL.md +16 -0
- package/skills/findbugs/SKILL.md +35 -7
- package/skills/monitor-open-prs/SKILL.md +2 -1
- package/skills/pr-converge/SKILL.md +3 -1
- package/skills/pr-converge/config/constants.py +1 -0
- package/skills/pr-converge/reference/per-tick.md +17 -0
- package/skills/pr-converge/scripts/check_bugbot_ci.py +113 -8
- package/skills/pr-converge/scripts/test_check_bugbot_ci.py +312 -0
- package/skills/qbug/SKILL.md +33 -8
package/skills/findbugs/SKILL.md
CHANGED
|
@@ -18,6 +18,16 @@ User types `/findbugs` or asks for a bug audit on the current branch's PR. Typic
|
|
|
18
18
|
|
|
19
19
|
If the current branch has no associated PR and no diff against the default branch, say so and stop. Do not invent scope.
|
|
20
20
|
|
|
21
|
+
## Refusals
|
|
22
|
+
|
|
23
|
+
First match wins; respond with the quoted line exactly and stop:
|
|
24
|
+
|
|
25
|
+
- **Disabled via environment.** When `CLAUDE_REVIEWS_DISABLED` contains the
|
|
26
|
+
token `bugteam` (comma-separated, case-insensitive, whitespace-tolerant):
|
|
27
|
+
`/findbugs is disabled via CLAUDE_REVIEWS_DISABLED.` `/findbugs` is a PR
|
|
28
|
+
bug-audit skill in the same family as `/bugteam` and `/qbug`, so the
|
|
29
|
+
shared `bugteam` token disables all three.
|
|
30
|
+
|
|
21
31
|
## The Process
|
|
22
32
|
|
|
23
33
|
### Step 1: Resolve PR scope
|
|
@@ -120,13 +130,31 @@ returns findings without posting them as inline comments is invisible
|
|
|
120
130
|
to the gate. Findbugs remains read-only on code — the review post is
|
|
121
131
|
the only side effect.
|
|
122
132
|
|
|
123
|
-
**Self-PR
|
|
124
|
-
`REQUEST_CHANGES` reviews when the authenticated identity
|
|
125
|
-
PR author
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
133
|
+
**Self-PR auto-toggle.** GitHub rejects both `APPROVE` and
|
|
134
|
+
`REQUEST_CHANGES` reviews with HTTP 422 when the authenticated identity
|
|
135
|
+
matches the PR author ("Cannot approve/request changes on your own pull
|
|
136
|
+
request"). `post_audit_thread.py` detects this case via `gh api user` +
|
|
137
|
+
`gh api repos/<o>/<r>/pulls/<n>` and auto-resolves an alternate gh
|
|
138
|
+
account's token for the reviews POST — the active `gh auth` account is
|
|
139
|
+
not mutated; only the bearer token sent on the request changes. After
|
|
140
|
+
the POST the active account is still whoever it was before, so no
|
|
141
|
+
"swap back" step is needed.
|
|
142
|
+
|
|
143
|
+
Configuration:
|
|
144
|
+
|
|
145
|
+
- `GH_TOKEN` / `GITHUB_TOKEN` env vars take precedence over the toggle.
|
|
146
|
+
Set them when you need to pin a specific reviewer identity by token
|
|
147
|
+
rather than by account login.
|
|
148
|
+
- `BUGTEAM_REVIEWER_ACCOUNT` env var names which authenticated alternate
|
|
149
|
+
to prefer when a toggle is needed (for example,
|
|
150
|
+
`BUGTEAM_REVIEWER_ACCOUNT=jl-cmd`). The env var name is shared across
|
|
151
|
+
every skill that invokes `post_audit_thread.py`. When unset, the
|
|
152
|
+
script falls back to the first alternate account `gh auth status`
|
|
153
|
+
reports.
|
|
154
|
+
- The named alternate must be logged in (`gh auth login -h github.com -u
|
|
155
|
+
<login>`) before the audit skill runs. The script exits 1 with a
|
|
156
|
+
pointing-at-`gh auth login` message when self-PR is detected and no
|
|
157
|
+
usable alternate is authenticated.
|
|
130
158
|
|
|
131
159
|
After the agent (and Haiku secondary) return and the merge is complete,
|
|
132
160
|
serialize the merged findings to a JSON file and call
|
|
@@ -27,6 +27,7 @@ description: >-
|
|
|
27
27
|
|
|
28
28
|
Refusals — first match wins; respond with the quoted line exactly and stop:
|
|
29
29
|
|
|
30
|
+
- **Disabled via environment.** When `CLAUDE_REVIEWS_DISABLED` (comma-separated, case-insensitive, whitespace-tolerant) contains the token `bugteam`: `/monitor-open-prs is a /bugteam dispatcher and /bugteam is disabled via CLAUDE_REVIEWS_DISABLED.`
|
|
30
31
|
- **GitHub API not accessible.** `get_me failed. /monitor-open-prs needs active GitHub MCP credentials.`
|
|
31
32
|
- **Dirty tree on the caller's repo.** `Uncommitted changes detected. Stash, commit, or revert before /monitor-open-prs.`
|
|
32
33
|
- **Required subagents missing.** Confirm `code-quality-agent` and `clean-coder` exist. Else: `Required subagent type <name> not installed.`
|
|
@@ -40,7 +41,7 @@ Call `scripts/discover_open_prs.discover_open_prs(all_owners=["jl-cmd", "JonEcho
|
|
|
40
41
|
For each discovered PR:
|
|
41
42
|
|
|
42
43
|
1. Resolve the PR's repo checkout (existing worktree or fresh `git clone`).
|
|
43
|
-
2. From that checkout, invoke `/bugteam --bugbot-retrigger <pr_number>`.
|
|
44
|
+
2. From that checkout, invoke `/bugteam --bugbot-retrigger <pr_number>`. When `CLAUDE_REVIEWS_DISABLED` (comma-separated, case-insensitive, whitespace-tolerant) contains the token `bugbot`, omit `--bugbot-retrigger` from the dispatched command so the bugbot leg sits out the run.
|
|
44
45
|
3. The `--bugbot-retrigger` flag tells bugteam to post `bugbot run` as an issue comment after every successful FIX push so Cursor's bugbot re-evaluates the new commit.
|
|
45
46
|
4. Bugteam runs its own 20-loop audit/fix cycle per PR; this skill waits for each bugteam invocation to return before dispatching the next (or fanning out — see below).
|
|
46
47
|
|
|
@@ -136,7 +136,9 @@ no longer applies.
|
|
|
136
136
|
- [ ] `bugbot_clean_at = current_head`
|
|
137
137
|
- [ ] Advance to Step 5
|
|
138
138
|
- [ ] **no review yet / commit_id mismatch** →
|
|
139
|
-
- [ ] Run `python ~/.claude/skills/pr-converge/scripts/check_bugbot_ci.py --owner <O> --repo <R> --check-
|
|
139
|
+
- [ ] Run `python ~/.claude/skills/pr-converge/scripts/check_bugbot_ci.py --owner <O> --repo <R> --check-clean --sha <current_head>`
|
|
140
|
+
- [ ] Exit 0 (bugbot CI completed with success/neutral conclusion and no review = silent pass) → `bugbot_clean_at = current_head` → advance to Step 5
|
|
141
|
+
- [ ] Exit 1 (not a silent pass) or Exit 2 (gh CLI error — silent pass not confirmable) → Run `python ~/.claude/skills/pr-converge/scripts/check_bugbot_ci.py --owner <O> --repo <R> --check-active --sha <current_head>`
|
|
140
142
|
- [ ] Exit 0 (already queued) → schedule 360s wakeup → return to Step 4 next tick
|
|
141
143
|
- [ ] Exit 1 → post exactly `bugbot run` via `add_issue_comment` (no `@cursor[bot]` mention, no other text), wait 8s
|
|
142
144
|
- [ ] Run `python ~/.claude/skills/pr-converge/scripts/check_bugbot_ci.py --owner <O> --repo <R> --sha <current_head>`
|
|
@@ -27,6 +27,7 @@ BUGBOT_DIRTY_BODY_REGEX = (
|
|
|
27
27
|
)
|
|
28
28
|
BUGBOT_CHECK_RUN_NAME_SUBSTRING = "bugbot"
|
|
29
29
|
ALL_BUGBOT_CHECK_RUN_ACTIVE_STATUSES = ("queued", "in_progress")
|
|
30
|
+
BUGBOT_CHECK_RUN_COMPLETED_STATUS = "completed"
|
|
30
31
|
ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS = ("success", "neutral")
|
|
31
32
|
BUGBOT_RUN_TRIGGER_PHRASE = "bugbot run\n"
|
|
32
33
|
BUGBOT_RUN_TRIGGER_WAIT_SECONDS = 8
|
|
@@ -207,6 +207,14 @@ BUGBOT.
|
|
|
207
207
|
|
|
208
208
|
## Step 3: Re-trigger bugbot
|
|
209
209
|
|
|
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.
|
|
215
|
+
- [ ] **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
|
+
- [ ] Exit 0 → bugbot CI completed clean with no review (silent pass); set `bugbot_clean_at = current_head`, `phase = BUGTEAM`, continue BUGTEAM same tick
|
|
217
|
+
- [ ] Exit 1 (not a silent pass) or Exit 2 (gh CLI error — silent pass not confirmable) → continue with the trigger flow below
|
|
210
218
|
- [ ] Run `python ~/.claude/skills/pr-converge/scripts/check_bugbot_ci.py --check-active --owner <O> --repo <R> --sha <current_head>`
|
|
211
219
|
- [ ] Exit 0 → bugbot already queued on this commit; skip posting, wait for completion
|
|
212
220
|
- [ ] Exit 1 → post trigger via `add_issue_comment(owner="OWNER", repo="REPO", issueNumber=NUMBER, body="bugbot run")`
|
|
@@ -215,6 +223,15 @@ BUGBOT.
|
|
|
215
223
|
- [ ] Exit non-zero → bugbot is down; set `bugbot_down = true`, `phase = BUGTEAM`, continue BUGTEAM same tick
|
|
216
224
|
- [ ] Exit 0 (check run present) → record `bugbot_acknowledged_at = <now ISO 8601>`, proceed to Step 4
|
|
217
225
|
|
|
226
|
+
The silent-pass pre-check fires FIRST so we never re-trigger a bot that
|
|
227
|
+
already finished cleanly. Cursor Bugbot communicates "no findings" by
|
|
228
|
+
completing the CI check with `conclusion: success` (or `neutral`) and
|
|
229
|
+
posting no review. The pre-check treats that outcome as
|
|
230
|
+
`bugbot_clean_at = current_head`, equivalent to an explicit clean
|
|
231
|
+
review. Without it, the trigger flow would re-prompt a bot that has
|
|
232
|
+
already evaluated this commit and refuses to re-run, and the bypass
|
|
233
|
+
branch would falsely mark `bugbot_down = true`.
|
|
234
|
+
|
|
218
235
|
`bugbot run` is empirically the only re-trigger Cursor Bugbot recognizes;
|
|
219
236
|
alternative phrasings silently no-op.
|
|
220
237
|
|
|
@@ -2,11 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
Usage:
|
|
4
4
|
python scripts/check_bugbot_ci.py --owner <O> --repo <R> --sha <SHA>
|
|
5
|
+
python scripts/check_bugbot_ci.py --owner <O> --repo <R> --sha <SHA> --check-active
|
|
6
|
+
python scripts/check_bugbot_ci.py --owner <O> --repo <R> --sha <SHA> --check-clean
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
Default mode (no flag):
|
|
7
9
|
0 — bugbot check run found (printed to stdout as JSON)
|
|
8
10
|
1 — no bugbot check run found
|
|
9
11
|
EXIT_CODE_GH_ERROR — gh CLI error
|
|
12
|
+
|
|
13
|
+
``--check-active`` mode:
|
|
14
|
+
0 — bugbot check run is queued or in_progress
|
|
15
|
+
1 — bugbot check run is absent or no longer active
|
|
16
|
+
|
|
17
|
+
``--check-clean`` mode (silent-pass detection):
|
|
18
|
+
0 — bugbot check run is completed with success/neutral conclusion
|
|
19
|
+
1 — bugbot check run is absent, still active, or completed with a
|
|
20
|
+
non-clean conclusion (failure, action_required, etc.)
|
|
21
|
+
EXIT_CODE_GH_ERROR — gh CLI error
|
|
10
22
|
"""
|
|
11
23
|
|
|
12
24
|
from __future__ import annotations
|
|
@@ -23,6 +35,8 @@ if str(_pr_converge_dir) not in sys.path:
|
|
|
23
35
|
|
|
24
36
|
from config.constants import (
|
|
25
37
|
ALL_BUGBOT_CHECK_RUN_ACTIVE_STATUSES,
|
|
38
|
+
ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS,
|
|
39
|
+
BUGBOT_CHECK_RUN_COMPLETED_STATUS,
|
|
26
40
|
BUGBOT_CHECK_RUN_NAME_SUBSTRING,
|
|
27
41
|
CHECK_RUNS_PER_PAGE,
|
|
28
42
|
EXIT_CODE_GH_ERROR,
|
|
@@ -121,6 +135,74 @@ def is_bugbot_run_active(*, owner: str, repo: str, sha: str) -> bool:
|
|
|
121
135
|
return False
|
|
122
136
|
|
|
123
137
|
|
|
138
|
+
def _classify_bugbot_check_run(
|
|
139
|
+
completed_process: subprocess.CompletedProcess[str],
|
|
140
|
+
) -> bool | None:
|
|
141
|
+
"""Classify the bugbot check run state from a gh API process result.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
completed_process: Result of calling ``_run_check_runs_api``.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
True when the captured stdout contains a bugbot check run with a
|
|
148
|
+
``completed`` status and a conclusion in
|
|
149
|
+
``ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS``. False when no such
|
|
150
|
+
check run is present (absent, still active, or completed with a
|
|
151
|
+
non-clean conclusion). None when ``completed_process.returncode``
|
|
152
|
+
is non-zero, signalling a gh CLI failure that the caller must
|
|
153
|
+
surface separately from "not clean".
|
|
154
|
+
"""
|
|
155
|
+
if completed_process.returncode != 0:
|
|
156
|
+
return None
|
|
157
|
+
for each_line in completed_process.stdout.splitlines():
|
|
158
|
+
stripped_line = each_line.strip()
|
|
159
|
+
if not stripped_line:
|
|
160
|
+
continue
|
|
161
|
+
try:
|
|
162
|
+
check_entry: dict[str, object] = json.loads(stripped_line)
|
|
163
|
+
except json.JSONDecodeError:
|
|
164
|
+
continue
|
|
165
|
+
each_name: object = check_entry.get("name")
|
|
166
|
+
if not isinstance(each_name, str):
|
|
167
|
+
continue
|
|
168
|
+
if BUGBOT_CHECK_RUN_NAME_SUBSTRING.lower() not in each_name.lower():
|
|
169
|
+
continue
|
|
170
|
+
each_status: object = check_entry.get("status")
|
|
171
|
+
if each_status != BUGBOT_CHECK_RUN_COMPLETED_STATUS:
|
|
172
|
+
return False
|
|
173
|
+
each_conclusion: object = check_entry.get("conclusion")
|
|
174
|
+
return (
|
|
175
|
+
isinstance(each_conclusion, str)
|
|
176
|
+
and each_conclusion in ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS
|
|
177
|
+
)
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def is_bugbot_run_clean(*, owner: str, repo: str, sha: str) -> bool | None:
|
|
182
|
+
"""Check whether bugbot has a completed check run with a clean conclusion.
|
|
183
|
+
|
|
184
|
+
A "silent pass" is bugbot's signal that it found no issues: the CI
|
|
185
|
+
check run completes with a ``success`` or ``neutral`` conclusion and
|
|
186
|
+
no review comment is posted. This function detects that signal so
|
|
187
|
+
callers can treat it as equivalent to an explicit clean review.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
owner: GitHub repository owner.
|
|
191
|
+
repo: GitHub repository name.
|
|
192
|
+
sha: Commit SHA to check.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
True when a bugbot check run is completed with a conclusion in
|
|
196
|
+
``ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS``. False when the
|
|
197
|
+
check run is absent, still active, or completed with a non-clean
|
|
198
|
+
conclusion. None when the gh CLI returns an error so the caller
|
|
199
|
+
can distinguish a transient API failure from a "not clean"
|
|
200
|
+
result.
|
|
201
|
+
"""
|
|
202
|
+
completed_process = _run_check_runs_api(owner=owner, repo=repo, sha=sha)
|
|
203
|
+
return _classify_bugbot_check_run(completed_process)
|
|
204
|
+
|
|
205
|
+
|
|
124
206
|
def parse_arguments(all_argv: list[str]) -> argparse.Namespace:
|
|
125
207
|
"""Parse command-line arguments.
|
|
126
208
|
|
|
@@ -128,18 +210,28 @@ def parse_arguments(all_argv: list[str]) -> argparse.Namespace:
|
|
|
128
210
|
all_argv: Command-line argument list.
|
|
129
211
|
|
|
130
212
|
Returns:
|
|
131
|
-
Parsed namespace with owner, repo, and
|
|
213
|
+
Parsed namespace with owner, repo, sha, and mode flags.
|
|
132
214
|
"""
|
|
133
215
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
134
216
|
parser.add_argument("--owner", required=True, help="GitHub repository owner")
|
|
135
217
|
parser.add_argument("--repo", required=True, help="GitHub repository name")
|
|
136
218
|
parser.add_argument("--sha", required=True, help="Commit SHA to check")
|
|
137
|
-
parser.
|
|
219
|
+
mode_group = parser.add_mutually_exclusive_group()
|
|
220
|
+
mode_group.add_argument(
|
|
138
221
|
"--check-active",
|
|
139
222
|
action="store_true",
|
|
140
223
|
default=False,
|
|
141
224
|
help="Check for active (queued/in-progress) check runs only",
|
|
142
225
|
)
|
|
226
|
+
mode_group.add_argument(
|
|
227
|
+
"--check-clean",
|
|
228
|
+
action="store_true",
|
|
229
|
+
default=False,
|
|
230
|
+
help=(
|
|
231
|
+
"Check for a completed bugbot check run with a "
|
|
232
|
+
"success/neutral conclusion (silent-pass detection)"
|
|
233
|
+
),
|
|
234
|
+
)
|
|
143
235
|
return parser.parse_args(all_argv)
|
|
144
236
|
|
|
145
237
|
|
|
@@ -150,19 +242,32 @@ def main(all_arguments: list[str]) -> int:
|
|
|
150
242
|
all_arguments: Command-line arguments.
|
|
151
243
|
|
|
152
244
|
Returns:
|
|
153
|
-
|
|
154
|
-
|
|
245
|
+
Exit code per the mode-specific contract documented in the
|
|
246
|
+
module docstring.
|
|
155
247
|
"""
|
|
156
248
|
arguments = parse_arguments(all_arguments)
|
|
249
|
+
if arguments.check_clean:
|
|
250
|
+
completed_process = _run_check_runs_api(
|
|
251
|
+
owner=arguments.owner,
|
|
252
|
+
repo=arguments.repo,
|
|
253
|
+
sha=arguments.sha,
|
|
254
|
+
)
|
|
255
|
+
if completed_process.returncode != 0:
|
|
256
|
+
print(f"gh api error: {completed_process.stderr}", file=sys.stderr)
|
|
257
|
+
return EXIT_CODE_GH_ERROR
|
|
258
|
+
is_clean = _classify_bugbot_check_run(completed_process)
|
|
259
|
+
if is_clean is not True:
|
|
260
|
+
print("bugbot: not clean")
|
|
261
|
+
return 0 if is_clean is True else 1
|
|
157
262
|
if arguments.check_active:
|
|
158
|
-
|
|
263
|
+
is_active = is_bugbot_run_active(
|
|
159
264
|
owner=arguments.owner,
|
|
160
265
|
repo=arguments.repo,
|
|
161
266
|
sha=arguments.sha,
|
|
162
267
|
)
|
|
163
|
-
if not
|
|
268
|
+
if not is_active:
|
|
164
269
|
print("bugbot: not found")
|
|
165
|
-
return 0 if
|
|
270
|
+
return 0 if is_active else 1
|
|
166
271
|
return check_bugbot_ci(
|
|
167
272
|
owner=arguments.owner,
|
|
168
273
|
repo=arguments.repo,
|
|
@@ -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
|
package/skills/qbug/SKILL.md
CHANGED
|
@@ -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
|
|
226
|
-
`REQUEST_CHANGES` reviews when the authenticated
|
|
227
|
-
the PR author
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
+
POST — the 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" \
|