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
|
@@ -1,95 +1,158 @@
|
|
|
1
|
-
#
|
|
1
|
+
# PR Description Guide
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
5
|
+
## Three shapes, picked from the diff
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Pick the shape from the size and risk of the change, not from a template.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
+
Prefer the smaller shape when in doubt.
|
|
15
16
|
|
|
16
|
-
|
|
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
|
-
|
|
19
|
+
One sentence. No Markdown headers. Optional `Fixes #N` line.
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
- Note any breaking changes prominently
|
|
21
|
+
```
|
|
22
|
+
Pin third-party GitHub Actions references to immutable commit SHAs.
|
|
23
|
+
```
|
|
27
24
|
|
|
28
|
-
|
|
25
|
+
```
|
|
26
|
+
Bump pinned Bun from 1.3.6 to 1.3.14.
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
Fixes #1311.
|
|
29
|
+
```
|
|
31
30
|
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
- Backward compatibility status
|
|
41
|
-
- Potential side effects
|
|
42
|
-
- Migration steps (if needed)
|
|
37
|
+
Fixes #<n>.
|
|
43
38
|
|
|
44
|
-
##
|
|
39
|
+
## Changes
|
|
45
40
|
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
- Note any follow-up work needed
|
|
45
|
+
## Test plan
|
|
50
46
|
|
|
51
|
-
|
|
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
|
-
|
|
52
|
+
The intro paragraph carries the Why -- no `## Why` header needed when one paragraph is enough.
|
|
54
53
|
|
|
55
|
-
|
|
54
|
+
## Shape 3: Heavy
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
-
|
|
59
|
-
|
|
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
|
-
|
|
60
|
+
Fixes #<n>.
|
|
62
61
|
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
68
|
+
```
|
|
69
|
+
<example error or reproduction>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Fix
|
|
77
73
|
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
##
|
|
85
|
-
Problem/context and reference to related issue (#123).
|
|
81
|
+
## Verification
|
|
86
82
|
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
- Command 1
|
|
84
|
+
- Command 2 (with output count when useful: "666 pass, 0 fail")
|
|
85
|
+
- Manual scenarios walked through
|
|
89
86
|
|
|
90
|
-
##
|
|
91
|
-
How this was tested and verified.
|
|
87
|
+
## Caveat
|
|
92
88
|
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
body_lower = body.lower()
|
|
307
|
+
"""Audit a PR body for substantive-prose and vague-language violations.
|
|
275
308
|
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
312
|
+
Returns:
|
|
313
|
+
A list of human-readable violation messages. Empty when the body passes.
|
|
314
|
+
"""
|
|
315
|
+
violations = []
|
|
284
316
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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"
|
|
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
|
-
"
|
|
29
|
-
"
|
|
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
|
|
125
|
+
def test_validate_passes_anthropic_standard_body() -> None:
|
|
113
126
|
assert validate_pr_body(VALID_BODY) == []
|
|
114
127
|
|
|
115
128
|
|
|
116
|
-
def
|
|
117
|
-
|
|
118
|
-
assert
|
|
119
|
-
|
|
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("
|
|
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
package/skills/bugteam/SKILL.md
CHANGED
|
@@ -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
|
|
54
|
-
`REQUEST_CHANGES` reviews when the authenticated identity
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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:
|