claude-dev-env 1.37.1 → 1.38.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/CLAUDE.md +3 -0
- package/_shared/pr-loop/audit-contract.md +4 -3
- package/_shared/pr-loop/fix-protocol.md +2 -0
- package/_shared/pr-loop/gh-payloads.md +38 -37
- package/_shared/pr-loop/scripts/README.md +0 -1
- package/_shared/pr-loop/scripts/preflight.py +2 -1
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +2 -2
- package/_shared/pr-loop/scripts/tests/test_preflight.py +22 -0
- package/_shared/pr-loop/state-schema.md +10 -10
- package/agents/clean-coder.md +4 -0
- package/agents/code-quality-agent.md +23 -85
- package/agents/groq-coder.md +8 -6
- package/hooks/blocking/__init__.py +0 -0
- package/hooks/blocking/hedging_language_blocker.py +2 -2
- package/hooks/blocking/state_description_blocker.py +243 -0
- package/hooks/blocking/tdd_enforcer.py +94 -0
- package/hooks/blocking/test_hedging_language_blocker.py +1 -1
- package/hooks/blocking/test_state_description_blocker.py +618 -0
- package/hooks/blocking/test_tdd_enforcer.py +152 -0
- package/hooks/config/state_description_blocker_constants.py +130 -0
- package/hooks/hooks.json +10 -0
- package/package.json +1 -1
- package/rules/no-historical-clutter.md +31 -10
- package/scripts/config/groq_bugteam_config.py +13 -5
- package/skills/bugteam/CONSTRAINTS.md +20 -27
- package/skills/bugteam/EXAMPLES.md +1 -1
- package/skills/bugteam/PROMPTS.md +60 -31
- package/skills/bugteam/SKILL.md +47 -47
- package/skills/bugteam/SKILL_EVALS.md +8 -8
- package/skills/bugteam/reference/github-pr-reviews.md +31 -31
- package/skills/bugteam/reference/team-setup.md +1 -1
- package/skills/bugteam/reference/teardown-publish-permissions.md +4 -4
- package/skills/copilot-review/SKILL.md +7 -14
- package/skills/findbugs/SKILL.md +2 -2
- package/skills/fixbugs/SKILL.md +1 -1
- package/skills/monitor-open-prs/SKILL.md +6 -6
- package/skills/pr-converge/SKILL.md +7 -6
- package/skills/pr-converge/reference/convergence-gates.md +28 -30
- package/skills/pr-converge/reference/examples.md +4 -4
- package/skills/pr-converge/reference/fix-protocol.md +6 -8
- package/skills/pr-converge/reference/multi-pr-orchestration.md +10 -10
- package/skills/pr-converge/reference/per-tick.md +18 -33
- package/skills/pr-converge/reference/stop-conditions.md +7 -7
- package/skills/pr-converge/scripts/README.md +65 -117
- package/skills/pr-review-responder/EXAMPLES.md +2 -2
- package/skills/pr-review-responder/PRINCIPLES.md +2 -8
- package/skills/pr-review-responder/README.md +7 -48
- package/skills/pr-review-responder/SKILL.md +2 -3
- package/skills/pr-review-responder/TESTING.md +8 -65
- package/skills/qbug/SKILL.md +10 -16
- package/_shared/pr-loop/scripts/config/gh_util_constants.py +0 -31
- package/_shared/pr-loop/scripts/gh_util.py +0 -193
- package/_shared/pr-loop/scripts/tests/test_gh_util.py +0 -257
- package/_shared/pr-loop/scripts/tests/test_gh_util_constants.py +0 -61
- package/skills/pr-converge/scripts/check_pr_mergeability.py +0 -78
- package/skills/pr-converge/scripts/config/pr_converge_constants.py +0 -134
- package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +0 -152
- package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +0 -70
- package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +0 -57
- package/skills/pr-converge/scripts/fetch_claude_inline_comments.py +0 -70
- package/skills/pr-converge/scripts/fetch_claude_reviews.py +0 -61
- package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +0 -70
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +0 -61
- package/skills/pr-converge/scripts/mark_pr_ready.py +0 -54
- package/skills/pr-converge/scripts/post-bugbot-run.helpers.ps1 +0 -49
- package/skills/pr-converge/scripts/post-bugbot-run.ps1 +0 -33
- package/skills/pr-converge/scripts/reply_to_inline_comment.py +0 -84
- package/skills/pr-converge/scripts/request_copilot_review.py +0 -71
- package/skills/pr-converge/scripts/resolve_pr_head.py +0 -58
- package/skills/pr-converge/scripts/review_field_helpers.py +0 -43
- package/skills/pr-converge/scripts/reviewer_fetch_core.py +0 -153
- package/skills/pr-converge/scripts/reviewer_specs.py +0 -98
- package/skills/pr-converge/scripts/test_check_pr_mergeability.py +0 -126
- package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +0 -443
- package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +0 -299
- package/skills/pr-converge/scripts/test_fetch_claude_inline_comments.py +0 -485
- package/skills/pr-converge/scripts/test_fetch_claude_reviews.py +0 -368
- package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +0 -440
- package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +0 -366
- package/skills/pr-converge/scripts/test_mark_pr_ready.py +0 -69
- package/skills/pr-converge/scripts/test_post_bugbot_run.py +0 -195
- package/skills/pr-converge/scripts/test_reply_to_inline_comment.py +0 -159
- package/skills/pr-converge/scripts/test_request_copilot_review.py +0 -101
- package/skills/pr-converge/scripts/test_resolve_pr_head.py +0 -79
- package/skills/pr-converge/scripts/test_review_field_helpers.py +0 -80
- package/skills/pr-converge/scripts/test_reviewer_fetch_core.py +0 -448
- package/skills/pr-converge/scripts/test_reviewer_specs.py +0 -107
- package/skills/pr-converge/scripts/test_trigger_bugbot.py +0 -139
- package/skills/pr-converge/scripts/test_view_pr_context.py +0 -155
- package/skills/pr-converge/scripts/trigger_bugbot.py +0 -77
- package/skills/pr-converge/scripts/view_pr_context.py +0 -78
- package/skills/pr-review-responder/scripts/respond_to_reviews.py +0 -376
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
"""Mark a draft PR as ready for review.
|
|
2
|
-
|
|
3
|
-
Convergence action invoked by pr-converge when both bugbot and bugteam are
|
|
4
|
-
clean against the same HEAD.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import argparse
|
|
8
|
-
import subprocess
|
|
9
|
-
import sys
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
|
|
12
|
-
if str(Path(__file__).resolve().parent) not in sys.path:
|
|
13
|
-
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
14
|
-
|
|
15
|
-
from evict_cached_config_modules import evict_cached_config_modules
|
|
16
|
-
|
|
17
|
-
evict_cached_config_modules()
|
|
18
|
-
|
|
19
|
-
from config.pr_converge_constants import GH_REPO_ARG_TEMPLATE
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def mark_pr_ready(*, owner: str, repo: str, number: int) -> None:
|
|
23
|
-
"""Run `gh pr ready <number> --repo <owner>/<repo>`."""
|
|
24
|
-
repo_arg = GH_REPO_ARG_TEMPLATE.format(owner=owner, repo=repo)
|
|
25
|
-
gh_command: list[str] = [
|
|
26
|
-
"gh",
|
|
27
|
-
"pr",
|
|
28
|
-
"ready",
|
|
29
|
-
str(number),
|
|
30
|
-
"--repo",
|
|
31
|
-
repo_arg,
|
|
32
|
-
]
|
|
33
|
-
subprocess.run(
|
|
34
|
-
gh_command,
|
|
35
|
-
capture_output=True,
|
|
36
|
-
check=True,
|
|
37
|
-
text=True,
|
|
38
|
-
encoding="utf-8",
|
|
39
|
-
errors="replace",
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def main() -> int:
|
|
44
|
-
parser = argparse.ArgumentParser(description=__doc__)
|
|
45
|
-
parser.add_argument("--owner", required=True)
|
|
46
|
-
parser.add_argument("--repo", required=True)
|
|
47
|
-
parser.add_argument("--number", required=True, type=int)
|
|
48
|
-
parsed_arguments = parser.parse_args()
|
|
49
|
-
mark_pr_ready(owner=parsed_arguments.owner, repo=parsed_arguments.repo, number=parsed_arguments.number)
|
|
50
|
-
return 0
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if __name__ == "__main__":
|
|
54
|
-
sys.exit(main())
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
function Resolve-InvocationMode {
|
|
2
|
-
param([string] $PullRequestInput, [string] $RepositoryInput, [int] $NumberInput)
|
|
3
|
-
|
|
4
|
-
if ($NumberInput -gt 0) {
|
|
5
|
-
if ([string]::IsNullOrWhiteSpace($RepositoryInput)) {
|
|
6
|
-
throw 'When -Number is set, -Repository must be owner/repo (for example jl-cmd/claude-code-config).'
|
|
7
|
-
}
|
|
8
|
-
return @{ Mode = 'RepoNumber'; Repository = $RepositoryInput; Number = $NumberInput }
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
$trimmed = $PullRequestInput.Trim()
|
|
12
|
-
if (-not [string]::IsNullOrWhiteSpace($trimmed)) {
|
|
13
|
-
return @{ Mode = 'Uri'; PullRequest = $trimmed }
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
throw 'Provide a pull request URL, owner/repo#number as the first argument, or -Repository with -Number.'
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function Build-GhArgumentList {
|
|
20
|
-
param([hashtable] $Invocation, [string] $BodyFilePath)
|
|
21
|
-
|
|
22
|
-
if ($Invocation.Mode -eq 'RepoNumber') {
|
|
23
|
-
return @(
|
|
24
|
-
'pr', 'comment',
|
|
25
|
-
$Invocation.Number.ToString(),
|
|
26
|
-
'-R', $Invocation.Repository,
|
|
27
|
-
'--body-file', $BodyFilePath
|
|
28
|
-
)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
$trimmed = $Invocation.PullRequest
|
|
32
|
-
if ($trimmed -match '^https://github\.com/[^/]+/[^/]+/pull/\d+$') {
|
|
33
|
-
return @('pr', 'comment', $trimmed, '--body-file', $BodyFilePath)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if ($trimmed -match '^([^/]+)/([^/#]+)#(\d+)$') {
|
|
37
|
-
$owner = $Matches[1]
|
|
38
|
-
$repository_name = $Matches[2]
|
|
39
|
-
$pull_number = $Matches[3]
|
|
40
|
-
return @(
|
|
41
|
-
'pr', 'comment',
|
|
42
|
-
$pull_number,
|
|
43
|
-
'-R', ('{0}/{1}' -f $owner, $repository_name),
|
|
44
|
-
'--body-file', $BodyFilePath
|
|
45
|
-
)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
throw ('Unrecognized PullRequest "{0}". Use a https://github.com/owner/repo/pull/NN URL or owner/repo#NN.' -f $trimmed)
|
|
49
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
[CmdletBinding()]
|
|
2
|
-
param(
|
|
3
|
-
[Parameter(Position = 0, HelpMessage = 'GitHub pull request URL or owner/repo#number.')]
|
|
4
|
-
[string] $PullRequest = '',
|
|
5
|
-
[Parameter(HelpMessage = 'owner/repo when using -Number instead of a URL or owner/repo#number.')]
|
|
6
|
-
[string] $Repository = '',
|
|
7
|
-
[Parameter(HelpMessage = 'Pull request number; requires -Repository.')]
|
|
8
|
-
[int] $Number = 0
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
$helpers_path = Join-Path $PSScriptRoot 'post-bugbot-run.helpers.ps1'
|
|
12
|
-
. $helpers_path
|
|
13
|
-
|
|
14
|
-
$LiteralBugbotRunBody = "bugbot run`n"
|
|
15
|
-
|
|
16
|
-
$invocation = Resolve-InvocationMode -PullRequestInput $PullRequest -RepositoryInput $Repository -NumberInput $Number
|
|
17
|
-
$scratch_temp_path = [System.IO.Path]::GetTempFileName()
|
|
18
|
-
$body_file_path = [System.IO.Path]::ChangeExtension($scratch_temp_path, '.md')
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
$utf8_without_byte_order_mark = New-Object System.Text.UTF8Encoding $false
|
|
22
|
-
[System.IO.File]::WriteAllText($body_file_path, $LiteralBugbotRunBody, $utf8_without_byte_order_mark)
|
|
23
|
-
|
|
24
|
-
$null = Get-Command gh -ErrorAction Stop
|
|
25
|
-
$argument_list = Build-GhArgumentList -Invocation $invocation -BodyFilePath $body_file_path
|
|
26
|
-
& gh @argument_list
|
|
27
|
-
if ($LASTEXITCODE -ne 0) {
|
|
28
|
-
throw ('gh exited with code {0}.' -f $LASTEXITCODE)
|
|
29
|
-
}
|
|
30
|
-
} finally {
|
|
31
|
-
Remove-Item -LiteralPath $body_file_path -Force -ErrorAction SilentlyContinue
|
|
32
|
-
Remove-Item -LiteralPath $scratch_temp_path -Force -ErrorAction SilentlyContinue
|
|
33
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
"""Post an inline reply to a PR review comment.
|
|
2
|
-
|
|
3
|
-
Reply body is sourced from a file via `gh api ... -F body=@<path>` (per the
|
|
4
|
-
gh-body-file rule — passing a string body to gh can corrupt backticks).
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import argparse
|
|
8
|
-
import json
|
|
9
|
-
import subprocess
|
|
10
|
-
import sys
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
|
|
13
|
-
if str(Path(__file__).resolve().parent) not in sys.path:
|
|
14
|
-
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
15
|
-
|
|
16
|
-
from evict_cached_config_modules import evict_cached_config_modules
|
|
17
|
-
|
|
18
|
-
evict_cached_config_modules()
|
|
19
|
-
|
|
20
|
-
from config.pr_converge_constants import (
|
|
21
|
-
GH_FIELD_BODY_AT_PREFIX,
|
|
22
|
-
GH_INLINE_COMMENT_REPLY_PATH_TEMPLATE,
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def reply_to_inline_comment(
|
|
27
|
-
*,
|
|
28
|
-
owner: str,
|
|
29
|
-
repo: str,
|
|
30
|
-
number: int,
|
|
31
|
-
comment_id: int,
|
|
32
|
-
body_file: Path,
|
|
33
|
-
) -> int:
|
|
34
|
-
"""POST an inline reply to a PR review comment, return gh's reply id."""
|
|
35
|
-
replies_endpoint = GH_INLINE_COMMENT_REPLY_PATH_TEMPLATE.format(
|
|
36
|
-
owner=owner, repo=repo, number=number, comment_id=comment_id
|
|
37
|
-
)
|
|
38
|
-
gh_command: list[str] = [
|
|
39
|
-
"gh",
|
|
40
|
-
"api",
|
|
41
|
-
"-X",
|
|
42
|
-
"POST",
|
|
43
|
-
replies_endpoint,
|
|
44
|
-
"-F",
|
|
45
|
-
f"{GH_FIELD_BODY_AT_PREFIX}{body_file}",
|
|
46
|
-
]
|
|
47
|
-
completed = subprocess.run(
|
|
48
|
-
gh_command,
|
|
49
|
-
capture_output=True,
|
|
50
|
-
check=True,
|
|
51
|
-
text=True,
|
|
52
|
-
encoding="utf-8",
|
|
53
|
-
errors="replace",
|
|
54
|
-
)
|
|
55
|
-
response_payload: dict[str, object] = json.loads(completed.stdout)
|
|
56
|
-
raw_reply_id = response_payload["id"]
|
|
57
|
-
if not isinstance(raw_reply_id, (int, str)):
|
|
58
|
-
raise TypeError(
|
|
59
|
-
f"gh response id field is not int|str: {type(raw_reply_id).__name__}"
|
|
60
|
-
)
|
|
61
|
-
return int(raw_reply_id)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def main() -> int:
|
|
65
|
-
parser = argparse.ArgumentParser(description=__doc__)
|
|
66
|
-
parser.add_argument("--owner", required=True)
|
|
67
|
-
parser.add_argument("--repo", required=True)
|
|
68
|
-
parser.add_argument("--number", required=True, type=int)
|
|
69
|
-
parser.add_argument("--comment-id", required=True, type=int, dest="comment_id")
|
|
70
|
-
parser.add_argument("--body-file", required=True, type=Path, dest="body_file")
|
|
71
|
-
parsed_arguments = parser.parse_args()
|
|
72
|
-
reply_id = reply_to_inline_comment(
|
|
73
|
-
owner=parsed_arguments.owner,
|
|
74
|
-
repo=parsed_arguments.repo,
|
|
75
|
-
number=parsed_arguments.number,
|
|
76
|
-
comment_id=parsed_arguments.comment_id,
|
|
77
|
-
body_file=parsed_arguments.body_file,
|
|
78
|
-
)
|
|
79
|
-
sys.stdout.write(f"{reply_id}\n")
|
|
80
|
-
return 0
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if __name__ == "__main__":
|
|
84
|
-
sys.exit(main())
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
"""Request a Copilot review on the current PR via the requested_reviewers API.
|
|
2
|
-
|
|
3
|
-
The reviewer ID literal is ``copilot-pull-request-reviewer[bot]`` - the
|
|
4
|
-
``[bot]`` suffix is load-bearing per ``skills/copilot-review/SKILL.md``;
|
|
5
|
-
``Copilot``, ``copilot``, and ``github-copilot`` all silently no-op on this
|
|
6
|
-
endpoint. After this POST returns, GitHub schedules Copilot to render a review
|
|
7
|
-
on the current HEAD; the caller polls ``fetch_copilot_reviews.py`` to converge.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import argparse
|
|
11
|
-
import subprocess
|
|
12
|
-
import sys
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
|
|
15
|
-
if str(Path(__file__).resolve().parent) not in sys.path:
|
|
16
|
-
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
17
|
-
|
|
18
|
-
from evict_cached_config_modules import evict_cached_config_modules
|
|
19
|
-
|
|
20
|
-
evict_cached_config_modules()
|
|
21
|
-
|
|
22
|
-
from config.pr_converge_constants import (
|
|
23
|
-
COPILOT_REVIEWER_REQUEST_ID,
|
|
24
|
-
GH_REQUESTED_REVIEWERS_FIELD_TEMPLATE,
|
|
25
|
-
GH_REQUESTED_REVIEWERS_PATH_TEMPLATE,
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def request_copilot_review(*, owner: str, repo: str, number: int) -> None:
|
|
30
|
-
"""POST a Copilot review request to the PR's requested_reviewers endpoint."""
|
|
31
|
-
requested_reviewers_endpoint = GH_REQUESTED_REVIEWERS_PATH_TEMPLATE.format(
|
|
32
|
-
owner=owner, repo=repo, number=number
|
|
33
|
-
)
|
|
34
|
-
reviewer_field_value = GH_REQUESTED_REVIEWERS_FIELD_TEMPLATE.format(
|
|
35
|
-
reviewer_id=COPILOT_REVIEWER_REQUEST_ID
|
|
36
|
-
)
|
|
37
|
-
gh_command: list[str] = [
|
|
38
|
-
"gh",
|
|
39
|
-
"api",
|
|
40
|
-
"-X",
|
|
41
|
-
"POST",
|
|
42
|
-
requested_reviewers_endpoint,
|
|
43
|
-
"-f",
|
|
44
|
-
reviewer_field_value,
|
|
45
|
-
]
|
|
46
|
-
subprocess.run(
|
|
47
|
-
gh_command,
|
|
48
|
-
capture_output=True,
|
|
49
|
-
check=True,
|
|
50
|
-
text=True,
|
|
51
|
-
encoding="utf-8",
|
|
52
|
-
errors="replace",
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def main() -> int:
|
|
57
|
-
parser = argparse.ArgumentParser(description=__doc__)
|
|
58
|
-
parser.add_argument("--owner", required=True)
|
|
59
|
-
parser.add_argument("--repo", required=True)
|
|
60
|
-
parser.add_argument("--number", required=True, type=int)
|
|
61
|
-
parsed_arguments = parser.parse_args()
|
|
62
|
-
request_copilot_review(
|
|
63
|
-
owner=parsed_arguments.owner,
|
|
64
|
-
repo=parsed_arguments.repo,
|
|
65
|
-
number=parsed_arguments.number,
|
|
66
|
-
)
|
|
67
|
-
return 0
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if __name__ == "__main__":
|
|
71
|
-
sys.exit(main())
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
"""Resolve the current HEAD SHA of a pull request.
|
|
2
|
-
|
|
3
|
-
Calls the single-object PR endpoint (`repos/{owner}/{repo}/pulls/{number}`) which
|
|
4
|
-
is NOT paginated, so `--paginate` / `--slurp` are unnecessary and `gh`'s
|
|
5
|
-
built-in `--jq` is safe to use here.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import argparse
|
|
9
|
-
import subprocess
|
|
10
|
-
import sys
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
|
|
13
|
-
if str(Path(__file__).resolve().parent) not in sys.path:
|
|
14
|
-
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
15
|
-
|
|
16
|
-
from evict_cached_config_modules import evict_cached_config_modules
|
|
17
|
-
|
|
18
|
-
evict_cached_config_modules()
|
|
19
|
-
|
|
20
|
-
from config.pr_converge_constants import GH_PR_OBJECT_PATH_TEMPLATE
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def resolve_pr_head(*, owner: str, repo: str, number: int) -> str:
|
|
24
|
-
"""Return the head_sha for the given PR."""
|
|
25
|
-
pr_endpoint = GH_PR_OBJECT_PATH_TEMPLATE.format(
|
|
26
|
-
owner=owner, repo=repo, number=number
|
|
27
|
-
)
|
|
28
|
-
gh_command: list[str] = [
|
|
29
|
-
"gh",
|
|
30
|
-
"api",
|
|
31
|
-
pr_endpoint,
|
|
32
|
-
"--jq",
|
|
33
|
-
".head.sha",
|
|
34
|
-
]
|
|
35
|
-
completed = subprocess.run(
|
|
36
|
-
gh_command,
|
|
37
|
-
capture_output=True,
|
|
38
|
-
check=True,
|
|
39
|
-
text=True,
|
|
40
|
-
encoding="utf-8",
|
|
41
|
-
errors="replace",
|
|
42
|
-
)
|
|
43
|
-
return completed.stdout.strip()
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def main() -> int:
|
|
47
|
-
parser = argparse.ArgumentParser(description=__doc__)
|
|
48
|
-
parser.add_argument("--owner", required=True)
|
|
49
|
-
parser.add_argument("--repo", required=True)
|
|
50
|
-
parser.add_argument("--number", required=True, type=int)
|
|
51
|
-
parsed_arguments = parser.parse_args()
|
|
52
|
-
head_sha = resolve_pr_head(owner=parsed_arguments.owner, repo=parsed_arguments.repo, number=parsed_arguments.number)
|
|
53
|
-
sys.stdout.write(f"{head_sha}\n")
|
|
54
|
-
return 0
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if __name__ == "__main__":
|
|
58
|
-
sys.exit(main())
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
"""Shared field-extraction helpers for GitHub PR review and inline-comment payloads.
|
|
2
|
-
|
|
3
|
-
The four ``fetch_*_reviews.py`` and ``fetch_*_inline_comments.py`` scripts in
|
|
4
|
-
this directory each parse the same JSON shapes and need the same defensive
|
|
5
|
-
field-coercion logic for ``user.login``, ``body``, ``submitted_at``, and
|
|
6
|
-
``state``. Centralizing the helpers here keeps a single source of truth and
|
|
7
|
-
prevents the bug-fix-in-one-copy-only failure mode flagged on PR #337.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def login_of(field_by_key: dict[str, object]) -> str | None:
|
|
12
|
-
"""Return the ``user.login`` string from a review/comment payload, or ``None``."""
|
|
13
|
-
user_field = field_by_key.get("user")
|
|
14
|
-
if not isinstance(user_field, dict):
|
|
15
|
-
return None
|
|
16
|
-
login_field = user_field.get("login")
|
|
17
|
-
if not isinstance(login_field, str):
|
|
18
|
-
return None
|
|
19
|
-
return login_field
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def body_of(field_by_key: dict[str, object]) -> str:
|
|
23
|
-
"""Return the ``body`` string from a review/comment payload, or ``""`` when missing."""
|
|
24
|
-
body_field = field_by_key.get("body")
|
|
25
|
-
if not isinstance(body_field, str):
|
|
26
|
-
return ""
|
|
27
|
-
return body_field
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def submitted_at_of(field_by_key: dict[str, object]) -> str:
|
|
31
|
-
"""Return the ``submitted_at`` string from a review payload, or ``""`` when missing."""
|
|
32
|
-
submitted_at_field = field_by_key.get("submitted_at")
|
|
33
|
-
if not isinstance(submitted_at_field, str):
|
|
34
|
-
return ""
|
|
35
|
-
return submitted_at_field
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def state_of(field_by_key: dict[str, object]) -> str:
|
|
39
|
-
"""Return the ``state`` string from a review payload, or ``""`` when missing."""
|
|
40
|
-
state_field = field_by_key.get("state")
|
|
41
|
-
if not isinstance(state_field, str):
|
|
42
|
-
return ""
|
|
43
|
-
return state_field
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
"""Shared fetch primitives for PR reviewer bots (Bugbot, Copilot, Claude).
|
|
2
|
-
|
|
3
|
-
The reviewer-specific scripts (``fetch_bugbot_reviews.py``,
|
|
4
|
-
``fetch_copilot_reviews.py``, ``fetch_claude_reviews.py`` and their
|
|
5
|
-
inline-comment counterparts) are thin entry points that pass a ``ReviewerSpec``
|
|
6
|
-
to these functions. The spec carries the substring used to recognise the
|
|
7
|
-
reviewer's GitHub login (case-insensitive substring match - required because
|
|
8
|
-
some bots emit different login strings at the review-level vs inline-comment
|
|
9
|
-
endpoints) and the per-reviewer classify callable.
|
|
10
|
-
|
|
11
|
-
Wraps the gh CLI invocations required by the gh-paginate rule:
|
|
12
|
-
``gh api '...?per_page=100' --paginate --slurp`` piped through external Python
|
|
13
|
-
JSON handling (instead of ``gh --jq``, which runs per-page and breaks
|
|
14
|
-
cross-page operations like sort/reverse - see GitHub CLI issue 10459).
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
|
|
19
|
-
import json
|
|
20
|
-
import subprocess
|
|
21
|
-
import sys
|
|
22
|
-
from pathlib import Path
|
|
23
|
-
|
|
24
|
-
if str(Path(__file__).resolve().parent) not in sys.path:
|
|
25
|
-
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
26
|
-
|
|
27
|
-
from evict_cached_config_modules import evict_cached_config_modules
|
|
28
|
-
|
|
29
|
-
evict_cached_config_modules()
|
|
30
|
-
|
|
31
|
-
from config.pr_converge_constants import (
|
|
32
|
-
GH_INLINE_COMMENTS_PATH_TEMPLATE,
|
|
33
|
-
GH_REVIEWS_PATH_TEMPLATE,
|
|
34
|
-
)
|
|
35
|
-
from review_field_helpers import body_of, login_of, state_of, submitted_at_of
|
|
36
|
-
from reviewer_specs import ReviewerSpec
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _login_matches_substring(
|
|
40
|
-
field_by_key: dict[str, object], login_filter_substring: str
|
|
41
|
-
) -> bool:
|
|
42
|
-
author_login = login_of(field_by_key) or ""
|
|
43
|
-
return login_filter_substring.lower() in author_login.lower()
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _run_gh_paginated(*, endpoint_path: str) -> list[dict[str, object]]:
|
|
47
|
-
gh_command: list[str] = [
|
|
48
|
-
"gh",
|
|
49
|
-
"api",
|
|
50
|
-
endpoint_path,
|
|
51
|
-
"--paginate",
|
|
52
|
-
"--slurp",
|
|
53
|
-
]
|
|
54
|
-
completed = subprocess.run(
|
|
55
|
-
gh_command,
|
|
56
|
-
capture_output=True,
|
|
57
|
-
check=True,
|
|
58
|
-
text=True,
|
|
59
|
-
encoding="utf-8",
|
|
60
|
-
errors="replace",
|
|
61
|
-
)
|
|
62
|
-
pages: list[list[dict[str, object]]] = json.loads(completed.stdout)
|
|
63
|
-
return [each_entry for each_page in pages for each_entry in each_page]
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def fetch_reviewer_reviews(
|
|
67
|
-
spec: ReviewerSpec,
|
|
68
|
-
*,
|
|
69
|
-
owner: str,
|
|
70
|
-
repo: str,
|
|
71
|
-
number: int,
|
|
72
|
-
) -> list[dict[str, object]]:
|
|
73
|
-
"""Return reviews from the matching reviewer newest-first, with classification.
|
|
74
|
-
|
|
75
|
-
Each entry contains ``review_id``, ``commit_id``, ``submitted_at``,
|
|
76
|
-
``state``, ``body``, and ``classification`` (``"clean"`` or ``"dirty"``).
|
|
77
|
-
Entries whose payload is missing ``submitted_at`` or ``id`` are dropped.
|
|
78
|
-
"""
|
|
79
|
-
reviews_endpoint = GH_REVIEWS_PATH_TEMPLATE.format(
|
|
80
|
-
owner=owner, repo=repo, number=number
|
|
81
|
-
)
|
|
82
|
-
all_flat_reviews = _run_gh_paginated(endpoint_path=reviews_endpoint)
|
|
83
|
-
all_matching_reviews = [
|
|
84
|
-
each_review
|
|
85
|
-
for each_review in all_flat_reviews
|
|
86
|
-
if _login_matches_substring(each_review, spec.login_filter_substring)
|
|
87
|
-
and each_review.get("submitted_at") is not None
|
|
88
|
-
and each_review.get("id") is not None
|
|
89
|
-
]
|
|
90
|
-
all_matching_reviews.sort(
|
|
91
|
-
key=lambda each_review: submitted_at_of(each_review), reverse=True
|
|
92
|
-
)
|
|
93
|
-
return [
|
|
94
|
-
{
|
|
95
|
-
"review_id": each_review["id"],
|
|
96
|
-
"commit_id": each_review.get("commit_id"),
|
|
97
|
-
"submitted_at": each_review["submitted_at"],
|
|
98
|
-
"state": state_of(each_review),
|
|
99
|
-
"body": body_of(each_review),
|
|
100
|
-
"classification": spec.classify_review(each_review),
|
|
101
|
-
}
|
|
102
|
-
for each_review in all_matching_reviews
|
|
103
|
-
]
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def fetch_reviewer_inline_comments(
|
|
107
|
-
spec: ReviewerSpec,
|
|
108
|
-
*,
|
|
109
|
-
owner: str,
|
|
110
|
-
repo: str,
|
|
111
|
-
number: int,
|
|
112
|
-
current_head: str,
|
|
113
|
-
all_reviews: list[dict[str, object]],
|
|
114
|
-
) -> list[dict[str, object]]:
|
|
115
|
-
"""Return inline comments anchored to the latest matching review on ``current_head``.
|
|
116
|
-
|
|
117
|
-
The ``all_reviews`` list is supplied by the caller (not fetched internally)
|
|
118
|
-
so the entry-point scripts retain a patchable seam: tests that patch
|
|
119
|
-
``fetch_X_reviews`` on the entry-point module continue to work because the
|
|
120
|
-
entry-point is what calls the reviews fetch.
|
|
121
|
-
|
|
122
|
-
Each entry contains ``comment_id``, ``commit_id``, ``path``, ``line``, and
|
|
123
|
-
``body``. Returns an empty list when no review in ``all_reviews`` is
|
|
124
|
-
anchored to ``current_head``.
|
|
125
|
-
"""
|
|
126
|
-
latest_review_for_head = next(
|
|
127
|
-
(
|
|
128
|
-
each_review
|
|
129
|
-
for each_review in all_reviews
|
|
130
|
-
if each_review.get("commit_id") == current_head
|
|
131
|
-
),
|
|
132
|
-
None,
|
|
133
|
-
)
|
|
134
|
-
if latest_review_for_head is None:
|
|
135
|
-
return []
|
|
136
|
-
target_pull_request_review_id = latest_review_for_head["review_id"]
|
|
137
|
-
comments_endpoint = GH_INLINE_COMMENTS_PATH_TEMPLATE.format(
|
|
138
|
-
owner=owner, repo=repo, number=number
|
|
139
|
-
)
|
|
140
|
-
all_flat_comments = _run_gh_paginated(endpoint_path=comments_endpoint)
|
|
141
|
-
return [
|
|
142
|
-
{
|
|
143
|
-
"comment_id": each_comment["id"],
|
|
144
|
-
"commit_id": each_comment.get("commit_id"),
|
|
145
|
-
"path": each_comment.get("path"),
|
|
146
|
-
"line": each_comment.get("line"),
|
|
147
|
-
"body": body_of(each_comment),
|
|
148
|
-
}
|
|
149
|
-
for each_comment in all_flat_comments
|
|
150
|
-
if _login_matches_substring(each_comment, spec.login_filter_substring)
|
|
151
|
-
and each_comment.get("commit_id") == current_head
|
|
152
|
-
and each_comment.get("pull_request_review_id") == target_pull_request_review_id
|
|
153
|
-
]
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
"""Reviewer specifications shared by the per-reviewer fetch entry-point scripts.
|
|
2
|
-
|
|
3
|
-
A ReviewerSpec carries the two knobs that vary across the bugbot, copilot, and
|
|
4
|
-
claude reviewers: the case-insensitive substring used to match the reviewer's
|
|
5
|
-
GitHub login, and the callable that classifies a single review payload as
|
|
6
|
-
``"clean"`` or ``"dirty"``. The spec instances declared at module scope are
|
|
7
|
-
imported by the thin entry-point wrappers (``fetch_bugbot_reviews.py`` etc.)
|
|
8
|
-
and by ``reviewer_fetch_core``.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
import re
|
|
14
|
-
import sys
|
|
15
|
-
from dataclasses import dataclass
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
from typing import Callable
|
|
18
|
-
|
|
19
|
-
if str(Path(__file__).resolve().parent) not in sys.path:
|
|
20
|
-
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
21
|
-
|
|
22
|
-
from evict_cached_config_modules import evict_cached_config_modules
|
|
23
|
-
|
|
24
|
-
evict_cached_config_modules()
|
|
25
|
-
|
|
26
|
-
from config.pr_converge_constants import (
|
|
27
|
-
ALL_CLAUDE_DIRTY_REVIEW_STATES,
|
|
28
|
-
ALL_COPILOT_DIRTY_REVIEW_STATES,
|
|
29
|
-
BUGBOT_DIRTY_BODY_REGEX,
|
|
30
|
-
CLAUDE_CLEAN_REVIEW_STATE,
|
|
31
|
-
CLAUDE_LOGIN_FILTER_SUBSTRING,
|
|
32
|
-
CLAUDE_SOFT_DIRTY_REVIEW_STATE,
|
|
33
|
-
COPILOT_CLEAN_REVIEW_STATE,
|
|
34
|
-
COPILOT_LOGIN_FILTER_SUBSTRING,
|
|
35
|
-
COPILOT_SOFT_DIRTY_REVIEW_STATE,
|
|
36
|
-
CURSOR_LOGIN_FILTER_SUBSTRING,
|
|
37
|
-
)
|
|
38
|
-
from review_field_helpers import body_of, state_of
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@dataclass(frozen=True)
|
|
42
|
-
class ReviewerSpec:
|
|
43
|
-
"""Per-reviewer configuration: login substring filter plus classify callable."""
|
|
44
|
-
|
|
45
|
-
login_filter_substring: str
|
|
46
|
-
classify_review: Callable[[dict[str, object]], str]
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _classify_bugbot_review(field_by_key: dict[str, object]) -> str:
|
|
50
|
-
review_body = body_of(field_by_key)
|
|
51
|
-
if re.search(BUGBOT_DIRTY_BODY_REGEX, review_body):
|
|
52
|
-
return "dirty"
|
|
53
|
-
return "clean"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _make_state_based_classifier(
|
|
57
|
-
*,
|
|
58
|
-
clean_state: str,
|
|
59
|
-
all_dirty_states: tuple[str, ...],
|
|
60
|
-
soft_dirty_state: str,
|
|
61
|
-
) -> Callable[[dict[str, object]], str]:
|
|
62
|
-
def classify_review(field_by_key: dict[str, object]) -> str:
|
|
63
|
-
review_state = state_of(field_by_key)
|
|
64
|
-
if review_state == clean_state:
|
|
65
|
-
return "clean"
|
|
66
|
-
if review_state not in all_dirty_states:
|
|
67
|
-
return "clean"
|
|
68
|
-
if review_state == soft_dirty_state and not body_of(field_by_key):
|
|
69
|
-
return "clean"
|
|
70
|
-
return "dirty"
|
|
71
|
-
|
|
72
|
-
return classify_review
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
bugbot_spec = ReviewerSpec(
|
|
76
|
-
login_filter_substring=CURSOR_LOGIN_FILTER_SUBSTRING,
|
|
77
|
-
classify_review=_classify_bugbot_review,
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
copilot_spec = ReviewerSpec(
|
|
82
|
-
login_filter_substring=COPILOT_LOGIN_FILTER_SUBSTRING,
|
|
83
|
-
classify_review=_make_state_based_classifier(
|
|
84
|
-
clean_state=COPILOT_CLEAN_REVIEW_STATE,
|
|
85
|
-
all_dirty_states=ALL_COPILOT_DIRTY_REVIEW_STATES,
|
|
86
|
-
soft_dirty_state=COPILOT_SOFT_DIRTY_REVIEW_STATE,
|
|
87
|
-
),
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
claude_spec = ReviewerSpec(
|
|
92
|
-
login_filter_substring=CLAUDE_LOGIN_FILTER_SUBSTRING,
|
|
93
|
-
classify_review=_make_state_based_classifier(
|
|
94
|
-
clean_state=CLAUDE_CLEAN_REVIEW_STATE,
|
|
95
|
-
all_dirty_states=ALL_CLAUDE_DIRTY_REVIEW_STATES,
|
|
96
|
-
soft_dirty_state=CLAUDE_SOFT_DIRTY_REVIEW_STATE,
|
|
97
|
-
),
|
|
98
|
-
)
|