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.
Files changed (27) hide show
  1. package/_shared/pr-loop/scripts/config/post_audit_thread_constants.py +10 -0
  2. package/_shared/pr-loop/scripts/config/reviews_disabled_constants.py +8 -0
  3. package/_shared/pr-loop/scripts/post_audit_thread.py +296 -1
  4. package/_shared/pr-loop/scripts/preflight.py +129 -2
  5. package/_shared/pr-loop/scripts/reviews_disabled.py +59 -0
  6. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +194 -1
  7. package/_shared/pr-loop/scripts/tests/test_preflight.py +41 -0
  8. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +36 -0
  9. package/agents/pr-description-writer.md +150 -52
  10. package/docs/PR_DESCRIPTION_GUIDE.md +127 -64
  11. package/hooks/blocking/pr_description_enforcer.py +57 -22
  12. package/hooks/blocking/test_pr_description_enforcer.py +69 -8
  13. package/hooks/config/pr_description_enforcer_constants.py +14 -0
  14. package/package.json +1 -1
  15. package/skills/bugteam/SKILL.md +28 -10
  16. package/skills/bugteam/reference/team-setup.md +5 -0
  17. package/skills/bugteam/scripts/bugteam_preflight.py +36 -2
  18. package/skills/bugteam/scripts/test_bugteam_preflight.py +41 -0
  19. package/skills/copilot-review/SKILL.md +16 -0
  20. package/skills/findbugs/SKILL.md +35 -7
  21. package/skills/monitor-open-prs/SKILL.md +2 -1
  22. package/skills/pr-converge/SKILL.md +3 -1
  23. package/skills/pr-converge/config/constants.py +1 -0
  24. package/skills/pr-converge/reference/per-tick.md +17 -0
  25. package/skills/pr-converge/scripts/check_bugbot_ci.py +113 -8
  26. package/skills/pr-converge/scripts/test_check_bugbot_ci.py +312 -0
  27. package/skills/qbug/SKILL.md +33 -8
@@ -1,95 +1,158 @@
1
- # GitHub PR Summary Writing Guide for AI
1
+ # PR Description Guide
2
2
 
3
- Use this guide when writing pull request descriptions. Follow these best practices to create clear, professional, and reviewable PR summaries.
3
+ This guide describes the PR-body shape that the `pr-description-writer` agent produces and that the `pr_description_enforcer` PreToolUse hook validates against. The style mirrors merged pull requests in `anthropics/claude-code`, `anthropics/claude-code-action`, and `anthropics/claude-cli-internal`.
4
4
 
5
- ## Required Sections
5
+ ## Three shapes, picked from the diff
6
6
 
7
- ### What (Changes)
7
+ Pick the shape from the size and risk of the change, not from a template.
8
8
 
9
- - Concise statement of what was changed
10
- - What files or systems were modified
11
- - What functionality was added, removed, or improved
12
- - Keep to 2-3 sentences maximum
9
+ | Signal | Shape |
10
+ |---|---|
11
+ | 1-3 files, mechanical change (pin bump, link fix, typo, single-line config), no behavior change | **Trivial** -- one declarative sentence, no headers |
12
+ | Behavior change, bug fix, small feature; under ~15 files | **Standard** -- intro paragraph + `## Changes` + `## Test plan` (or `## Validation`) |
13
+ | New subsystem, refactor across many files, schema or contract change, anything with a caveat | **Heavy** -- intro + `## Problem` + `## Fix` (or `## Changes`) + `## Verification` + extra sections as needed |
13
14
 
14
- ### Why (Problem/Context)
15
+ Prefer the smaller shape when in doubt.
15
16
 
16
- - Explain the problem this PR solves
17
- - Provide business or technical context
18
- - Reference related issue numbers using `#123` or `Fixes #123`, `Closes #456`
19
- - If no issue exists, briefly explain the motivation
17
+ ## Shape 1: Trivial
20
18
 
21
- ### How (Approach/Solution)
19
+ One sentence. No Markdown headers. Optional `Fixes #N` line.
22
20
 
23
- - Describe your implementation approach
24
- - Explain any design decisions or trade-offs
25
- - Include architectural changes if applicable
26
- - Note any breaking changes prominently
21
+ ```
22
+ Pin third-party GitHub Actions references to immutable commit SHAs.
23
+ ```
27
24
 
28
- ## Supporting Details
25
+ ```
26
+ Bump pinned Bun from 1.3.6 to 1.3.14.
29
27
 
30
- ### Testing and Quality
28
+ Fixes #1311.
29
+ ```
31
30
 
32
- - What tests were added/modified
33
- - How to manually verify the changes (if applicable)
34
- - Any areas of concern or limitations
35
- - Performance impact (if relevant)
31
+ ## Shape 2: Standard
36
32
 
37
- ### Dependencies and Risk
33
+ ```
34
+ <One short intro paragraph stating the change and why it matters.
35
+ Reference the failure mode or user-visible symptom when there is one.>
38
36
 
39
- - New dependencies introduced (if any)
40
- - Backward compatibility status
41
- - Potential side effects
42
- - Migration steps (if needed)
37
+ Fixes #<n>.
43
38
 
44
- ## Optional but Valuable
39
+ ## Changes
45
40
 
46
- ### Related Issues/PRs
41
+ - `path/to/file.ext`: short clause describing the change
42
+ - `path/to/other.ext`: short clause
43
+ - `tests/foo.test.ts`: 2 new cases for X
47
44
 
48
- - Link to dependent PRs or issues
49
- - Note any follow-up work needed
45
+ ## Test plan
50
46
 
51
- ### Screenshots/Examples (for UI changes)
47
+ - `bun test test/foo.test.ts`
48
+ - `bun run typecheck`
49
+ - Manual: reproduce on a branch named `feature/a,b`; confirm no rejection
50
+ ```
52
51
 
53
- - Before/after comparisons when visual changes are involved
52
+ The intro paragraph carries the Why -- no `## Why` header needed when one paragraph is enough.
54
53
 
55
- ### Reviewer Guidance
54
+ ## Shape 3: Heavy
56
55
 
57
- - Specific areas to focus on
58
- - Questions for reviewers
59
- - Deployment considerations
56
+ ```
57
+ <Two- to four-sentence intro: scope, motivation, user-visible effect.
58
+ Link to the prior PR or issue that motivates this one if applicable.>
60
59
 
61
- ## Tone and Style Guidelines
60
+ Fixes #<n>.
62
61
 
63
- - Be clear and concise -- reviewers scan quickly
64
- - Use second person sparingly -- focus on what the code does, not what the reviewer should do
65
- - Avoid jargon -- explain technical terms if non-obvious
66
- - Use markdown formatting -- bullets, code blocks, headers for readability
67
- - Be honest about limitations -- acknowledge trade-offs and known issues
68
- - Assume reviewers are unfamiliar -- provide sufficient context
62
+ ## Problem
69
63
 
70
- ## What to Avoid
64
+ <Concrete description of the failure mode or gap. Include the actual
65
+ error text, a reproduction, or the symptomatic log line in a fenced
66
+ code block when it helps.>
71
67
 
72
- - Vague statements like "fix bug" or "update code"
73
- - AI-generated summaries without human verification
74
- - Large walls of text -- break into sections
75
- - Repeating information from commit messages
76
- - References to temporary branch names or internal jargon without context
68
+ ```
69
+ <example error or reproduction>
70
+ ```
71
+
72
+ ## Fix
77
73
 
78
- ## Example Structure
74
+ <What the change does at the level a reviewer needs to evaluate it.
75
+ Reference the file or function by path/name. Don't restate the diff
76
+ line-by-line -- the reviewer can read the code.>
79
77
 
80
- ```markdown
81
- ## Description
82
- Brief 1-2 sentence overview of the change.
78
+ - `src/path/file.ts`: brief description
79
+ - `src/path/other.ts`: brief description
83
80
 
84
- ## Why
85
- Problem/context and reference to related issue (#123).
81
+ ## Verification
86
82
 
87
- ## How
88
- Implementation approach and design decisions.
83
+ - Command 1
84
+ - Command 2 (with output count when useful: "666 pass, 0 fail")
85
+ - Manual scenarios walked through
89
86
 
90
- ## Testing
91
- How this was tested and verified.
87
+ ## Caveat
92
88
 
93
- ## Risk Assessment
94
- Any breaking changes, dependencies, or concerns.
89
+ <Anything a reviewer or downstream user needs to know that isn't in
90
+ the diff. Omit this section when there is no caveat.>
95
91
  ```
92
+
93
+ Optional heavy-shape sections, used when they earn their place:
94
+
95
+ - `## Runtime behavior` -- when the change preserves behavior but moves it.
96
+ - `## Components` -- a small table when the PR introduces multiple named artifacts.
97
+ - `## Backward compatibility` -- when an older consumer might still hit this code path.
98
+ - `## Context` -- background a reviewer outside the area would need.
99
+
100
+ ## Section vocabulary
101
+
102
+ Pick from these. Don't invent new ones, and don't use synonyms within one PR:
103
+
104
+ | Intent | Header (pick one) |
105
+ |---|---|
106
+ | What this PR is and why | `## Summary` -- or no header (preferred when 1-3 sentences) |
107
+ | The failure being fixed | `## Problem` -- or no header when the intro paragraph carries it |
108
+ | The change itself | `## Changes` or `## Fix` |
109
+ | How it was verified | `## Test plan`, `## Validation`, `## Verification`, or `## Testing` |
110
+ | Things to know | `## Caveat`, `## Runtime behavior`, `## Backward compatibility`, `## Context` |
111
+
112
+ ## File reference style
113
+
114
+ - Always backtick file paths: `` `src/github/operations/branch.ts` ``.
115
+ - Use the full path from repo root, not just the basename, unless the basename is unambiguous within the PR.
116
+ - Bullet lists describing per-file changes lead with the backticked path and a colon:
117
+ - `` `src/foo.ts`: whitelists `,` in branch names ``
118
+ - `` `test/foo.test.ts`: 3 new cases for comma-bearing branches ``
119
+ - Prose calling out a single primary file bolds the backticked filename plus a colon: `` **`branch.ts`**: ... ``.
120
+
121
+ ## Cross-references
122
+
123
+ - Issue/PR shorthand: `#1311` (same repo), `anthropics/claude-code#40576` (cross-repo).
124
+ - `Fixes #N` and `Closes #N` close the linked issue on merge -- use them deliberately.
125
+ - `Linear: CC-1723` -- one line, no Markdown, after the intro paragraph or at the bottom.
126
+ - "Same change as <repo>#<n>" / "Follow-up to #<n>" -- one-liners that orient a reviewer.
127
+
128
+ ## Markers and footers
129
+
130
+ - `<!-- NO CHANGELOG -->` on its own line, at the very end, for docs-only or CI-only PRs in repos that auto-generate changelogs from PR titles.
131
+ - Don't add a "Generated with Claude Code" footer -- merged Anthropic PRs don't use one consistently, and the repo's commit trailer covers attribution.
132
+
133
+ ## What the hook checks
134
+
135
+ `pr_description_enforcer.py` runs on `gh pr create` and `gh pr edit` invocations that include a body. It blocks when any of the following are true:
136
+
137
+ - The body, after stripping Markdown ceremony (headers, code fences, bullet markers, bold/emphasis, link text), contains fewer than 40 characters of prose. A skeleton of `## Summary` + `## Changes` + bullets with no Why paragraph fails here.
138
+ - The body contains vague phrases like `fix bug`, `update code`, `minor changes`, or `various fixes`.
139
+
140
+ The hook does not require any specific section headers -- `## Summary`, `## Problem`, `## Fix`, `## Changes`, `## Test plan` are all optional, including any combination of them. A single substantive sentence ("Pin third-party GitHub Actions references to immutable commit SHAs.") satisfies the check.
141
+
142
+ When the hook blocks, it points the caller at the `pr-description-writer` agent and at this guide.
143
+
144
+ ## Tone
145
+
146
+ - Plain language. "The pull engine would blindly overwrite any record marked as 'synced'" -- not "PullEngine.run() exhibited non-idempotent behavior".
147
+ - Active voice. "Add `,` to the whitelist" -- not "`,` was added to the whitelist".
148
+ - No filler. Start with the content, not "This PR..." or "In this change...".
149
+ - No restating the diff. Trust the reviewer to read the code; explain the parts they can't infer.
150
+ - No hedging ("should", "might", "I think") unless the uncertainty is real -- in which case say "not yet verified" and call it out.
151
+
152
+ ## What to avoid
153
+
154
+ - Code snippets that simply repeat the diff. Code blocks are for error reproductions, failing commands, or before/after when the contrast is the point.
155
+ - Technical jargon for non-obvious internals ("Dexie transaction" -> "database transaction").
156
+ - Multi-line preamble. The PR title already says what the change is.
157
+ - Section headers over empty content. If `## Caveat` would be empty, drop the header.
158
+ - Second-person commentary directed at the reviewer ("please review carefully"). The reviewer knows their job.
@@ -20,16 +20,30 @@ from _gh_body_arg_utils import (
20
20
  iter_significant_tokens,
21
21
  )
22
22
 
23
- PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
24
- PR_GUIDE_PATH = os.path.join(PLUGIN_ROOT, "docs", "PR_DESCRIPTION_GUIDE.md")
25
23
 
26
- REQUIRED_PR_SECTION_HEADERS = [
27
- "description",
28
- "why",
29
- "how",
30
- ]
24
+ def _insert_hooks_tree_for_imports() -> None:
25
+ hooks_tree = Path(__file__).resolve().parent.parent
26
+ hooks_tree_string = str(hooks_tree)
27
+ if hooks_tree_string not in sys.path:
28
+ sys.path.insert(0, hooks_tree_string)
29
+
30
+
31
+ _insert_hooks_tree_for_imports()
32
+
33
+ from config.pr_description_enforcer_constants import (
34
+ BLOCKQUOTE_MARKER_PATTERN,
35
+ BOLD_PAIR_PATTERN,
36
+ BULLET_MARKER_PATTERN,
37
+ FENCED_CODE_BLOCK_PATTERN,
38
+ HEADING_LINE_PATTERN,
39
+ INLINE_CODE_PATTERN,
40
+ LINK_TEXT_PATTERN,
41
+ MINIMUM_SUBSTANTIVE_PROSE_CHARS,
42
+ WHITESPACE_RUN_PATTERN,
43
+ )
31
44
 
32
- MINIMUM_PR_BODY_LENGTH = 50
45
+ PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
46
+ PR_GUIDE_PATH = os.path.join(PLUGIN_ROOT, "docs", "PR_DESCRIPTION_GUIDE.md")
33
47
 
34
48
  VAGUE_LANGUAGE_PATTERN = re.compile(
35
49
  r'\b(fix(?:ed)? (?:bug|issue|it)|update(?:d)? code|minor changes|various (?:fixes|updates|improvements))\b',
@@ -269,23 +283,43 @@ def extract_body_from_command(
269
283
  return result
270
284
 
271
285
 
286
+ def _count_substantive_prose_chars(body: str) -> int:
287
+ """Return the count of prose characters after stripping Markdown ceremony.
288
+
289
+ Removes fenced code, inline code, heading lines, blockquote markers,
290
+ bullet list markers, bold/emphasis markers, and Markdown link targets.
291
+ Collapses internal whitespace so a body of only headers and bullets --
292
+ no real WHY paragraph -- registers as effectively empty.
293
+ """
294
+ body_without_fences = FENCED_CODE_BLOCK_PATTERN.sub('', body)
295
+ body_without_inline_code = INLINE_CODE_PATTERN.sub('', body_without_fences)
296
+ body_without_blockquotes = BLOCKQUOTE_MARKER_PATTERN.sub('', body_without_inline_code)
297
+ body_without_headings = HEADING_LINE_PATTERN.sub('', body_without_blockquotes)
298
+ body_without_bullets = BULLET_MARKER_PATTERN.sub('', body_without_headings)
299
+ body_without_bold = BOLD_PAIR_PATTERN.sub(r'\1', body_without_bullets)
300
+ body_without_emphasis = body_without_bold.replace('*', '')
301
+ body_without_links = LINK_TEXT_PATTERN.sub(r'\1', body_without_emphasis)
302
+ body_collapsed = WHITESPACE_RUN_PATTERN.sub(' ', body_without_links).strip()
303
+ return len(body_collapsed)
304
+
305
+
272
306
  def validate_pr_body(body: str) -> list[str]:
273
- violations = []
274
- body_lower = body.lower()
307
+ """Audit a PR body for substantive-prose and vague-language violations.
275
308
 
276
- missing_required_sections = [
277
- header for header in REQUIRED_PR_SECTION_HEADERS
278
- if f"## {header}" not in body_lower and f"**{header}" not in body_lower
279
- ]
309
+ Args:
310
+ body: The PR body markdown text to audit.
280
311
 
281
- if missing_required_sections:
282
- formatted_sections = ", ".join(f"'{each_section.title()}'" for each_section in missing_required_sections)
283
- violations.append(f"Missing required section(s): {formatted_sections}")
312
+ Returns:
313
+ A list of human-readable violation messages. Empty when the body passes.
314
+ """
315
+ violations = []
284
316
 
285
- stripped_body = re.sub(r'#.*', '', body).strip()
286
- stripped_body = re.sub(r'\*\*.*?\*\*', '', stripped_body).strip()
287
- if len(stripped_body) < MINIMUM_PR_BODY_LENGTH:
288
- violations.append("PR body too short -- provide meaningful context for reviewers")
317
+ substantive_chars = _count_substantive_prose_chars(body)
318
+ if substantive_chars < MINIMUM_SUBSTANTIVE_PROSE_CHARS:
319
+ violations.append(
320
+ "PR body lacks substantive prose -- include a Why paragraph or "
321
+ "substantive explanation, not only headers and bullets"
322
+ )
289
323
 
290
324
  vague_matches = VAGUE_LANGUAGE_PATTERN.findall(body)
291
325
  if vague_matches:
@@ -329,7 +363,8 @@ def main() -> None:
329
363
  pr_guide_reference = f" @{PR_GUIDE_PATH}" if os.path.exists(PR_GUIDE_PATH) else ""
330
364
  denial_reason = (
331
365
  f"BLOCKED: [PR_DESCRIPTION] {violation_list}. "
332
- f"Follow the PR description guide:{pr_guide_reference}"
366
+ f"Use the pr-description-writer agent to author the body in Anthropic claude-code style. "
367
+ f"Guide:{pr_guide_reference}"
333
368
  )
334
369
  result = {
335
370
  "hookSpecificOutput": {
@@ -25,8 +25,21 @@ extract_body_from_command = hook_module.extract_body_from_command
25
25
  validate_pr_body = hook_module.validate_pr_body
26
26
 
27
27
  VALID_BODY = (
28
- "## Description\n\nThis PR fixes a real bug.\n\n"
29
- "## Why\n\nBecause it was broken in production.\n\n"
28
+ "Allow commas in branch names so PRs whose head branch was generated from "
29
+ "a title or external identifier no longer fail validation before any git "
30
+ "operation.\n\n"
31
+ "Fixes #1300.\n\n"
32
+ "## Changes\n\n"
33
+ "- `src/github/operations/branch.ts`: add `,` to the whitelist regex\n"
34
+ "- `test/branch.test.ts`: 3 new cases covering comma-bearing branch names\n\n"
35
+ "## Test plan\n\n"
36
+ "- `bun test test/branch.test.ts`\n"
37
+ "- `bun run typecheck`\n"
38
+ )
39
+
40
+ LEGACY_DESCRIPTION_WHY_HOW_BODY = (
41
+ "## Description\n\nThis PR fixes a real bug in the authentication module.\n\n"
42
+ "## Why\n\nBecause it was broken in production and customers reported failures.\n\n"
30
43
  "## How\n\nRefactored the auth module to handle edge cases correctly.\n"
31
44
  )
32
45
 
@@ -109,15 +122,63 @@ def test_extract_short_flag_shell_var_returns_empty() -> None:
109
122
  assert extract_body_from_command(command) == ""
110
123
 
111
124
 
112
- def test_validate_passes_complete_body() -> None:
125
+ def test_validate_passes_anthropic_standard_body() -> None:
113
126
  assert validate_pr_body(VALID_BODY) == []
114
127
 
115
128
 
116
- def test_validate_blocks_missing_sections() -> None:
117
- violations = validate_pr_body("Some body text without required sections.\n" * 5)
118
- assert any(
119
- "Missing required section" in each_violation for each_violation in violations
129
+ def test_validate_passes_legacy_description_why_how_body() -> None:
130
+ """Existing Description/Why/How bodies must still pass -- the relaxed rule only widens what's accepted."""
131
+ assert validate_pr_body(LEGACY_DESCRIPTION_WHY_HOW_BODY) == []
132
+
133
+
134
+ def test_validate_passes_sectionless_prose_body() -> None:
135
+ """Anthropic's trivial-PR shape is one sentence with no headers."""
136
+ body = (
137
+ "Pin third-party GitHub Actions references to immutable commit SHAs "
138
+ "so a tag move cannot redirect CI to attacker-controlled code."
139
+ )
140
+ assert validate_pr_body(body) == []
141
+
142
+
143
+ def test_validate_blocks_skeleton_body_with_only_headers_and_bullets() -> None:
144
+ """Sections + bullets without any prose Why is rejected -- the substantive-prose check catches this."""
145
+ body = (
146
+ "## Summary\n\n"
147
+ "## Changes\n\n"
148
+ "- `a`\n"
149
+ "- `b`\n"
150
+ "- `c`\n"
151
+ )
152
+ violations = validate_pr_body(body)
153
+ assert any("substantive prose" in each_violation.lower() for each_violation in violations)
154
+
155
+
156
+ def test_validate_blocks_blockquoted_headings_with_no_real_prose() -> None:
157
+ """Regression: blockquote markers must strip BEFORE heading stripping.
158
+
159
+ A line like `> ## Summary` starts with `>`, so `^#+[ \\t].*$` cannot match it
160
+ in heading position. If blockquote markers are stripped after, the bare
161
+ `## Summary` text survives into the prose stream and inflates the count.
162
+ Correct order strips `> ` first, then the line becomes a real heading and
163
+ drops out, leaving an effectively empty body below the 40-character minimum.
164
+ """
165
+ body = "> ## Summary\n> ## Why\n> ## How"
166
+ violations = validate_pr_body(body)
167
+ assert any("substantive prose" in each_violation.lower() for each_violation in violations)
168
+
169
+
170
+ def test_validate_passes_prose_after_bare_hashes_with_no_space() -> None:
171
+ """Bug regression: `##\\n` followed by prose must not have its prose eaten by the heading regex.
172
+
173
+ The previous pattern `^#+\\s.*$` matched `\\s` against the newline, then `.*$` greedily
174
+ consumed the next line. The fix restricts the whitespace class to `[ \\t]` so only true
175
+ headings (`## text`) match, leaving prose-after-bare-hashes intact for substantive-prose counting.
176
+ """
177
+ body = (
178
+ "##\nThis is real prose that should not be eaten by the heading regex, "
179
+ "it should pass the 40-character minimum."
120
180
  )
181
+ assert validate_pr_body(body) == []
121
182
 
122
183
 
123
184
  def test_validate_blocks_vague_language() -> None:
@@ -128,7 +189,7 @@ def test_validate_blocks_vague_language() -> None:
128
189
 
129
190
  def test_validate_blocks_short_body() -> None:
130
191
  violations = validate_pr_body("Too short.")
131
- assert any("too short" in each_violation.lower() for each_violation in violations)
192
+ assert any("substantive prose" in each_violation.lower() for each_violation in violations)
132
193
 
133
194
 
134
195
  def test_body_file_content_validated(tmp_path: pathlib.Path) -> None:
@@ -0,0 +1,14 @@
1
+ """Configuration constants for the pr_description_enforcer PreToolUse hook."""
2
+
3
+ import re
4
+
5
+ MINIMUM_SUBSTANTIVE_PROSE_CHARS: int = 40
6
+
7
+ FENCED_CODE_BLOCK_PATTERN: re.Pattern[str] = re.compile(r"```.*?```", re.DOTALL)
8
+ INLINE_CODE_PATTERN: re.Pattern[str] = re.compile(r"`[^`]*`")
9
+ HEADING_LINE_PATTERN: re.Pattern[str] = re.compile(r"^#+[ \t].*$", re.MULTILINE)
10
+ BOLD_PAIR_PATTERN: re.Pattern[str] = re.compile(r"\*\*([^*]+?)\*\*")
11
+ BULLET_MARKER_PATTERN: re.Pattern[str] = re.compile(r"^\s*[-*+]\s+", re.MULTILINE)
12
+ BLOCKQUOTE_MARKER_PATTERN: re.Pattern[str] = re.compile(r"^\s*>\s+", re.MULTILINE)
13
+ LINK_TEXT_PATTERN: re.Pattern[str] = re.compile(r"\[([^\]]+)\]\([^)]+\)")
14
+ WHITESPACE_RUN_PATTERN: re.Pattern[str] = re.compile(r"\s+")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.39.0",
3
+ "version": "1.40.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,6 +32,11 @@ other failures require manual fix before Step 0. Full detail:
32
32
 
33
33
  First match wins; respond with the quoted line exactly and stop:
34
34
 
35
+ - **Disabled via environment.** When `CLAUDE_REVIEWS_DISABLED` contains the
36
+ token `bugteam` (comma-separated, case-insensitive, whitespace-tolerant):
37
+ `/bugteam is disabled via CLAUDE_REVIEWS_DISABLED.` The pre-flight script
38
+ also exits 7 in this case so any caller invoking it directly halts on the
39
+ same signal.
35
40
  - **No PR or upstream diff.** `No PR or upstream diff. /bugteam needs a target.`
36
41
  - **Dirty tree.** `Uncommitted changes detected. Stash, commit, or revert before
37
42
  /bugteam.`
@@ -50,16 +55,29 @@ Every internal audit pass (CLEAN or DIRTY) ends with one call to
50
55
  finding; each becomes its own resolvable thread). The mandate applies
51
56
  whether bugteam runs inside `/pr-converge` or standalone.
52
57
 
53
- **Self-PR precondition.** GitHub rejects both `APPROVE` and
54
- `REQUEST_CHANGES` reviews when the authenticated identity (the `gh auth`
55
- token in scope) matches the PR author with HTTP 422 ("Cannot
56
- approve/request changes on your own pull request"). `post_audit_thread.py`
57
- will retry on transient errors and then exit 2 (retry exhaustion); the
58
- script does not detect the self-PR case and downgrade to `COMMENT`. To
59
- run bugteam on a PR you authored, switch `gh auth` to an alternate
60
- reviewer identity (a separate GitHub account) BEFORE invoking the skill.
61
- Without this switch, exit 2 is a hard halt — there is no automated
62
- fallback path. The script does not auto-downgrade on the self-PR case.
58
+ **Self-PR auto-toggle.** GitHub rejects both `APPROVE` and
59
+ `REQUEST_CHANGES` reviews with HTTP 422 when the authenticated identity
60
+ matches the PR author ("Cannot approve/request changes on your own pull
61
+ request"). `post_audit_thread.py` detects this case via `gh api user` +
62
+ `gh api repos/<o>/<r>/pulls/<n>` and auto-resolves an alternate gh
63
+ account's token for the reviews POST the active `gh auth` account is
64
+ not mutated; only the bearer token sent on the request changes. After
65
+ the POST the active account is still whoever it was before, so no
66
+ "swap back" step is needed.
67
+
68
+ Configuration:
69
+
70
+ - `GH_TOKEN` / `GITHUB_TOKEN` env vars take precedence over the toggle.
71
+ Set them when you need to pin a specific reviewer identity by token
72
+ rather than by account login.
73
+ - `BUGTEAM_REVIEWER_ACCOUNT` env var names which authenticated alternate
74
+ to prefer when a toggle is needed (for example,
75
+ `BUGTEAM_REVIEWER_ACCOUNT=jl-cmd`). When unset, the script falls back
76
+ to the first alternate account `gh auth status` reports.
77
+ - The named alternate must be logged in (`gh auth login -h github.com -u
78
+ <login>`) before the audit skill runs. The script exits 1 with a
79
+ pointing-at-`gh auth login` message when self-PR is detected and no
80
+ usable alternate is authenticated.
63
81
 
64
82
  ```
65
83
  python "${CLAUDE_SKILL_DIR}/../../_shared/pr-loop/scripts/post_audit_thread.py" \
@@ -138,3 +138,8 @@ Self-claiming by task subject prefix keeps each teammate on its assigned PR.
138
138
  **`--bugbot-retrigger` flag:** when present, the FIX subagent posts a `bugbot
139
139
  run` issue comment via the Step 2.5 issue-comments fallback endpoint after
140
140
  every successful FIX push, to re-trigger Cursor's bugbot on the new commit.
141
+
142
+ **Opt-out gate.** When `CLAUDE_REVIEWS_DISABLED` (comma-separated,
143
+ case-insensitive, whitespace-tolerant) contains the token `bugbot`, the FIX
144
+ subagent skips the re-trigger post even when the flag is present. The rest of
145
+ the bugteam audit/fix cycle continues unchanged.
@@ -12,8 +12,11 @@ for each_cached_module_name in [
12
12
  if each_module_key == "config" or each_module_key.startswith("config.")
13
13
  ]:
14
14
  sys.modules.pop(each_cached_module_name, None)
15
- if str(Path(__file__).resolve().parent) not in sys.path:
16
- sys.path.insert(0, str(Path(__file__).resolve().parent))
15
+ _bugteam_scripts_directory = str(Path(__file__).absolute().parent)
16
+ while _bugteam_scripts_directory in sys.path:
17
+ sys.path.remove(_bugteam_scripts_directory)
18
+ if _bugteam_scripts_directory not in sys.path:
19
+ sys.path.insert(0, _bugteam_scripts_directory)
17
20
 
18
21
  from config.bugteam_preflight_constants import (
19
22
  ALL_DISCOVERY_IGNORE_DIRECTORIES,
@@ -32,6 +35,26 @@ from config.bugteam_preflight_constants import (
32
35
  PYTEST_INI_FILENAME,
33
36
  )
34
37
 
38
+ for each_cached_module_name in [
39
+ each_module_key
40
+ for each_module_key in list(sys.modules)
41
+ if each_module_key == "config" or each_module_key.startswith("config.")
42
+ ]:
43
+ sys.modules.pop(each_cached_module_name, None)
44
+ _shared_pr_loop_scripts_directory = (
45
+ Path(__file__).absolute().parent
46
+ / ".." / ".." / ".." / "_shared" / "pr-loop" / "scripts"
47
+ ).absolute()
48
+ if str(_shared_pr_loop_scripts_directory) not in sys.path:
49
+ sys.path.insert(0, str(_shared_pr_loop_scripts_directory))
50
+
51
+ from reviews_disabled import (
52
+ CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN,
53
+ CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME,
54
+ EXIT_CODE_BUGTEAM_DISABLED_VIA_ENV,
55
+ is_bugteam_disabled_via_env,
56
+ )
57
+
35
58
 
36
59
  def verify_git_hooks_path(repository_root: Path | None) -> int:
37
60
  """Check that core.hooksPath resolves to the claude-dev-env git-hooks directory.
@@ -259,6 +282,17 @@ def main(all_argv: list[str] | None = None) -> int:
259
282
  if os.environ.get(BUGTEAM_PREFLIGHT_SKIP_ENV_VAR_NAME, "").strip() == "1":
260
283
  print(f"{BUGTEAM_PREFLIGHT_PREFIX}skipped (BUGTEAM_PREFLIGHT_SKIP=1).", file=sys.stderr)
261
284
  return 0
285
+ reviews_disabled_env_var_name = CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME
286
+ reviews_disabled_bugteam_token = CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN
287
+ disabled_via_env_exit_code = EXIT_CODE_BUGTEAM_DISABLED_VIA_ENV
288
+ if is_bugteam_disabled_via_env():
289
+ print(
290
+ f"{BUGTEAM_PREFLIGHT_PREFIX}halted "
291
+ f"({reviews_disabled_env_var_name} contains "
292
+ f"'{reviews_disabled_bugteam_token}').",
293
+ file=sys.stderr,
294
+ )
295
+ return disabled_via_env_exit_code
262
296
  start = Path.cwd()
263
297
  resolved_repository_root: Path = (
264
298
  arguments.repo_root.resolve()
@@ -266,3 +266,44 @@ def test_has_pytest_configuration_returns_false_without_either_file(
266
266
  repository_root = tmp_path / "repo"
267
267
  repository_root.mkdir()
268
268
  assert bugteam_preflight.has_pytest_configuration(repository_root) is False
269
+
270
+
271
+ def test_main_should_halt_when_env_var_lists_bugteam(
272
+ monkeypatch: pytest.MonkeyPatch,
273
+ capsys: pytest.CaptureFixture[str],
274
+ ) -> None:
275
+ """CLAUDE_REVIEWS_DISABLED=bugteam must halt preflight with the dedicated exit code."""
276
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugteam")
277
+ monkeypatch.delenv("BUGTEAM_PREFLIGHT_SKIP", raising=False)
278
+ exit_code = bugteam_preflight.main(["--no-pytest"])
279
+ assert exit_code == bugteam_preflight.EXIT_CODE_BUGTEAM_DISABLED_VIA_ENV
280
+ captured = capsys.readouterr()
281
+ assert "CLAUDE_REVIEWS_DISABLED" in captured.err
282
+ assert "bugteam" in captured.err
283
+
284
+
285
+ def test_main_should_continue_when_env_var_omits_bugteam(
286
+ monkeypatch: pytest.MonkeyPatch,
287
+ tmp_path: Path,
288
+ ) -> None:
289
+ """CLAUDE_REVIEWS_DISABLED without the bugteam token must not halt preflight."""
290
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "copilot,bugbot")
291
+ monkeypatch.delenv("BUGTEAM_PREFLIGHT_SKIP", raising=False)
292
+ claude_hooks_path = tmp_path / ".claude" / "hooks" / "git-hooks"
293
+ claude_hooks_path.mkdir(parents=True)
294
+ with patch("subprocess.run") as mock_run:
295
+ mock_run.return_value = _make_completed_process(
296
+ str(claude_hooks_path) + "\n", returncode=0
297
+ )
298
+ exit_code = bugteam_preflight.main(["--no-pytest"])
299
+ assert exit_code != bugteam_preflight.EXIT_CODE_BUGTEAM_DISABLED_VIA_ENV
300
+
301
+
302
+ def test_main_should_halt_when_env_var_contains_uppercase_or_whitespace_bugteam_token(
303
+ monkeypatch: pytest.MonkeyPatch,
304
+ ) -> None:
305
+ """Token matching must be case-insensitive and whitespace-tolerant."""
306
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", " BugTeam , copilot ")
307
+ monkeypatch.delenv("BUGTEAM_PREFLIGHT_SKIP", raising=False)
308
+ exit_code = bugteam_preflight.main(["--no-pytest"])
309
+ assert exit_code == bugteam_preflight.EXIT_CODE_BUGTEAM_DISABLED_VIA_ENV
@@ -21,6 +21,22 @@ The user is on a PR branch, wants Copilot (the GitHub Copilot reviewer bot) to k
21
21
 
22
22
  ## The Process
23
23
 
24
+ ### Step 0: Opt-out check
25
+
26
+ Before any other work, inspect the `CLAUDE_REVIEWS_DISABLED` environment
27
+ variable. Treat the value as a comma-separated list of skill tokens
28
+ (case-insensitive, whitespace-tolerant). When the parsed list contains
29
+ `copilot`, respond with the literal line `/copilot-review is disabled via
30
+ CLAUDE_REVIEWS_DISABLED.` and stop — do not spawn the subagent, do not call
31
+ the Copilot reviewer API, do not run any other step of this skill.
32
+
33
+ PowerShell probe (Windows):
34
+
35
+ ```pwsh
36
+ $disabled = ($env:CLAUDE_REVIEWS_DISABLED -split ',' | ForEach-Object { $_.Trim().ToLowerInvariant() })
37
+ if ($disabled -contains 'copilot') { '/copilot-review is disabled via CLAUDE_REVIEWS_DISABLED.' }
38
+ ```
39
+
24
40
  ### Step 1: Gather PR context
25
41
 
26
42
  From the current repo: