claude-dev-env 1.36.2 → 1.37.1
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/preflight_constants.py +29 -8
- package/_shared/pr-loop/scripts/preflight.py +242 -20
- package/_shared/pr-loop/scripts/tests/test_preflight.py +362 -25
- package/_shared/pr-loop/scripts/tests/test_preflight_constants.py +9 -14
- package/hooks/blocking/code_rules_enforcer.py +269 -23
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +157 -1
- package/hooks/config/test_unused_module_import_constants.py +48 -0
- package/hooks/config/unused_module_import_constants.py +41 -0
- package/package.json +1 -1
- package/rules/gh-paginate.md +4 -50
- package/rules/no-historical-clutter.md +36 -0
- package/skills/bg-agent/SKILL.md +69 -0
- package/skills/bugteam/CONSTRAINTS.md +10 -19
- package/skills/bugteam/PROMPTS.md +21 -14
- package/skills/bugteam/SKILL.md +122 -208
- package/skills/bugteam/SKILL_EVALS.md +75 -114
- package/skills/bugteam/reference/README.md +2 -4
- package/skills/bugteam/reference/audit-and-teammates.md +21 -48
- package/skills/bugteam/reference/audit-contract.md +7 -7
- package/skills/bugteam/reference/design-rationale.md +3 -8
- package/skills/bugteam/reference/team-setup.md +11 -19
- package/skills/bugteam/reference/teardown-publish-permissions.md +2 -14
- package/skills/bugteam/scripts/config/__init__.py +0 -0
- package/skills/bugteam/scripts/config/reflow_skill_md_constants.py +12 -0
- package/skills/bugteam/scripts/reflow_skill_md.py +51 -47
- package/skills/bugteam/sources.md +1 -25
- package/skills/bugteam/test_skill_additions.py +4 -13
- package/skills/fresh-branch/SKILL.md +71 -0
- package/skills/gotcha/SKILL.md +73 -0
- package/skills/monitor-open-prs/SKILL.md +4 -37
- package/skills/monitor-open-prs/test_skill_contract.py +0 -5
- package/skills/pr-converge/SKILL.md +60 -1298
- package/skills/pr-converge/reference/convergence-gates.md +122 -0
- package/skills/pr-converge/reference/examples.md +76 -0
- package/skills/pr-converge/reference/fix-protocol.md +56 -0
- package/skills/pr-converge/reference/ground-rules.md +13 -0
- package/skills/pr-converge/reference/multi-pr-orchestration.md +204 -0
- package/skills/pr-converge/reference/per-tick.md +204 -0
- package/skills/pr-converge/reference/state-schema.md +19 -0
- package/skills/pr-converge/reference/stop-conditions.md +26 -0
- package/skills/pr-converge/scripts/README.md +36 -9
- package/skills/pr-converge/scripts/check_pr_mergeability.py +1 -2
- package/skills/pr-converge/scripts/config/pr_converge_constants.py +74 -5
- package/skills/pr-converge/scripts/config/reflow_skill_md_constants.py +13 -0
- package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +0 -24
- package/skills/pr-converge/scripts/cursor-agents-continue.ahk +22 -2
- package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +19 -59
- package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +15 -61
- package/skills/pr-converge/scripts/fetch_claude_inline_comments.py +70 -0
- package/skills/pr-converge/scripts/fetch_claude_reviews.py +61 -0
- package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +19 -61
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +14 -74
- package/skills/pr-converge/scripts/reflow_skill_md.py +71 -50
- package/skills/pr-converge/scripts/reviewer_fetch_core.py +153 -0
- package/skills/pr-converge/scripts/reviewer_specs.py +98 -0
- package/skills/pr-converge/scripts/test_cursor_agents_continue.py +65 -0
- package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +107 -6
- package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +85 -6
- package/skills/pr-converge/scripts/test_fetch_claude_inline_comments.py +485 -0
- package/skills/pr-converge/scripts/test_fetch_claude_reviews.py +368 -0
- package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +74 -6
- package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +94 -8
- package/skills/pr-converge/scripts/test_reflow_skill_md.py +162 -0
- package/skills/pr-converge/scripts/test_reviewer_fetch_core.py +448 -0
- package/skills/pr-converge/scripts/test_reviewer_specs.py +107 -0
- package/skills/pr-converge/scripts/test_view_pr_context.py +44 -0
- package/skills/pr-converge/scripts/view_pr_context.py +35 -4
- package/skills/pr-converge/workflows/schedule-wakeup-loop.md +24 -22
- package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +0 -113
- package/skills/bugteam/reference/workflow-path-b-task-harness.md +0 -48
- package/skills/bugteam/test_team_lifecycle.py +0 -103
- package/skills/monitor-open-prs/test_team_lifecycle.py +0 -46
- package/skills/pr-converge/scripts/open_followup_copilot_pr.py +0 -136
- package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +0 -236
- package/skills/pr-converge/test_team_lifecycle.py +0 -56
- package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +0 -108
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Fetch Claude reviewer-bot reviews newest-first, classified as dirty or clean.
|
|
2
|
+
|
|
3
|
+
Thin wrapper around ``reviewer_fetch_core.fetch_reviewer_reviews`` parameterised
|
|
4
|
+
by ``claude_spec``. Classification follows the review's ``state`` field
|
|
5
|
+
(``APPROVED`` -> clean; ``CHANGES_REQUESTED`` -> dirty; ``COMMENTED`` with
|
|
6
|
+
non-empty body -> dirty; everything else -> clean) - see ``reviewer_specs``.
|
|
7
|
+
|
|
8
|
+
Wraps the gh CLI invocation required by the gh-paginate rule:
|
|
9
|
+
``gh api '...?per_page=100' --paginate --slurp`` piped through external Python
|
|
10
|
+
JSON handling (instead of ``gh --jq``, which runs per-page and breaks
|
|
11
|
+
cross-page operations like sort/reverse - see GitHub CLI issue 10459).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
if str(Path(__file__).resolve().parent) not in sys.path:
|
|
22
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
23
|
+
|
|
24
|
+
from evict_cached_config_modules import evict_cached_config_modules
|
|
25
|
+
|
|
26
|
+
evict_cached_config_modules()
|
|
27
|
+
|
|
28
|
+
from reviewer_fetch_core import fetch_reviewer_reviews
|
|
29
|
+
from reviewer_specs import claude_spec
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def fetch_claude_reviews(
|
|
33
|
+
*,
|
|
34
|
+
owner: str,
|
|
35
|
+
repo: str,
|
|
36
|
+
number: int,
|
|
37
|
+
) -> list[dict[str, object]]:
|
|
38
|
+
"""Return Claude reviews newest-first, each with a classification."""
|
|
39
|
+
return fetch_reviewer_reviews(
|
|
40
|
+
claude_spec, owner=owner, repo=repo, number=number
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main() -> int:
|
|
45
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
46
|
+
parser.add_argument("--owner", required=True)
|
|
47
|
+
parser.add_argument("--repo", required=True)
|
|
48
|
+
parser.add_argument("--number", required=True, type=int)
|
|
49
|
+
parsed_arguments = parser.parse_args()
|
|
50
|
+
all_reviews = fetch_claude_reviews(
|
|
51
|
+
owner=parsed_arguments.owner,
|
|
52
|
+
repo=parsed_arguments.repo,
|
|
53
|
+
number=parsed_arguments.number,
|
|
54
|
+
)
|
|
55
|
+
json.dump(all_reviews, sys.stdout)
|
|
56
|
+
sys.stdout.write("\n")
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
sys.exit(main())
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
"""Fetch unaddressed Copilot inline comments for the latest Copilot review on a commit.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Thin wrapper around ``reviewer_fetch_core.fetch_reviewer_inline_comments``
|
|
4
|
+
parameterised by ``copilot_spec``. The ``fetch_copilot_reviews`` call lives
|
|
5
|
+
here (rather than inside the core) so tests can patch it on this module to
|
|
6
|
+
exercise the inline-comments fetch in isolation.
|
|
7
7
|
|
|
8
|
-
Wraps the gh CLI invocation required by the gh-paginate rule for the comments
|
|
9
|
-
``gh api`` on ``repos/{owner}/{repo}/pulls/{number}/comments`` with
|
|
8
|
+
Wraps the gh CLI invocation required by the gh-paginate rule for the comments
|
|
9
|
+
list: ``gh api`` on ``repos/{owner}/{repo}/pulls/{number}/comments`` with
|
|
10
|
+
``--paginate --slurp`` and external JSON handling.
|
|
10
11
|
"""
|
|
11
12
|
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
12
15
|
import argparse
|
|
13
16
|
import json
|
|
14
|
-
import subprocess
|
|
15
17
|
import sys
|
|
16
18
|
from pathlib import Path
|
|
17
19
|
|
|
@@ -22,12 +24,9 @@ from evict_cached_config_modules import evict_cached_config_modules
|
|
|
22
24
|
|
|
23
25
|
evict_cached_config_modules()
|
|
24
26
|
|
|
25
|
-
from config.pr_converge_constants import (
|
|
26
|
-
COPILOT_REVIEWER_LOGIN,
|
|
27
|
-
GH_INLINE_COMMENTS_PATH_TEMPLATE,
|
|
28
|
-
)
|
|
29
27
|
from fetch_copilot_reviews import fetch_copilot_reviews
|
|
30
|
-
from
|
|
28
|
+
from reviewer_fetch_core import fetch_reviewer_inline_comments
|
|
29
|
+
from reviewer_specs import copilot_spec
|
|
31
30
|
|
|
32
31
|
|
|
33
32
|
def fetch_copilot_inline_comments(
|
|
@@ -37,57 +36,16 @@ def fetch_copilot_inline_comments(
|
|
|
37
36
|
number: int,
|
|
38
37
|
current_head: str,
|
|
39
38
|
) -> list[dict[str, object]]:
|
|
40
|
-
"""Return Copilot inline comments for the latest Copilot review on ``current_head``.
|
|
41
|
-
|
|
42
|
-
Each entry contains comment_id, commit_id, path, line, and body.
|
|
43
|
-
"""
|
|
39
|
+
"""Return Copilot inline comments for the latest Copilot review on ``current_head``."""
|
|
44
40
|
all_copilot_reviews = fetch_copilot_reviews(owner=owner, repo=repo, number=number)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
)
|
|
53
|
-
if latest_copilot_review_for_head is None:
|
|
54
|
-
return []
|
|
55
|
-
target_pull_request_review_id = latest_copilot_review_for_head["review_id"]
|
|
56
|
-
comments_endpoint = GH_INLINE_COMMENTS_PATH_TEMPLATE.format(
|
|
57
|
-
owner=owner, repo=repo, number=number
|
|
58
|
-
)
|
|
59
|
-
gh_command: list[str] = [
|
|
60
|
-
"gh",
|
|
61
|
-
"api",
|
|
62
|
-
comments_endpoint,
|
|
63
|
-
"--paginate",
|
|
64
|
-
"--slurp",
|
|
65
|
-
]
|
|
66
|
-
completed = subprocess.run(
|
|
67
|
-
gh_command,
|
|
68
|
-
capture_output=True,
|
|
69
|
-
check=True,
|
|
70
|
-
text=True,
|
|
71
|
-
encoding="utf-8",
|
|
72
|
-
errors="replace",
|
|
41
|
+
return fetch_reviewer_inline_comments(
|
|
42
|
+
copilot_spec,
|
|
43
|
+
owner=owner,
|
|
44
|
+
repo=repo,
|
|
45
|
+
number=number,
|
|
46
|
+
current_head=current_head,
|
|
47
|
+
all_reviews=all_copilot_reviews,
|
|
73
48
|
)
|
|
74
|
-
pages: list[list[dict[str, object]]] = json.loads(completed.stdout)
|
|
75
|
-
all_flat_comments = [
|
|
76
|
-
each_comment for each_page in pages for each_comment in each_page
|
|
77
|
-
]
|
|
78
|
-
return [
|
|
79
|
-
{
|
|
80
|
-
"comment_id": each_comment["id"],
|
|
81
|
-
"commit_id": each_comment.get("commit_id"),
|
|
82
|
-
"path": each_comment.get("path"),
|
|
83
|
-
"line": each_comment.get("line"),
|
|
84
|
-
"body": body_of(each_comment),
|
|
85
|
-
}
|
|
86
|
-
for each_comment in all_flat_comments
|
|
87
|
-
if login_of(each_comment) == COPILOT_REVIEWER_LOGIN
|
|
88
|
-
and each_comment.get("commit_id") == current_head
|
|
89
|
-
and each_comment.get("pull_request_review_id") == target_pull_request_review_id
|
|
90
|
-
]
|
|
91
49
|
|
|
92
50
|
|
|
93
51
|
def main() -> int:
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
"""Fetch GitHub Copilot reviewer reviews newest-first, classified as dirty or clean.
|
|
2
2
|
|
|
3
|
+
Thin wrapper around ``reviewer_fetch_core.fetch_reviewer_reviews`` parameterised
|
|
4
|
+
by ``copilot_spec``. Classification follows the review's ``state`` field
|
|
5
|
+
(``APPROVED`` -> clean; ``CHANGES_REQUESTED`` -> dirty; ``COMMENTED`` with
|
|
6
|
+
non-empty body -> dirty; everything else -> clean) - see ``reviewer_specs``.
|
|
7
|
+
|
|
3
8
|
Wraps the gh CLI invocation required by the gh-paginate rule:
|
|
4
9
|
``gh api '...?per_page=100' --paginate --slurp`` piped through external Python
|
|
5
|
-
JSON handling (instead of ``gh --jq``, which runs per-page and breaks
|
|
6
|
-
operations like sort/reverse - see GitHub CLI
|
|
7
|
-
|
|
8
|
-
Classification follows the review's ``state`` field:
|
|
9
|
-
- ``APPROVED`` -> ``"clean"``
|
|
10
|
-
- ``CHANGES_REQUESTED`` -> ``"dirty"``
|
|
11
|
-
- ``COMMENTED`` with non-empty body -> ``"dirty"`` (Copilot uses COMMENTED + body
|
|
12
|
-
to flag findings without a hard block)
|
|
13
|
-
- everything else -> ``"clean"`` (no actionable findings on PR)
|
|
10
|
+
JSON handling (instead of ``gh --jq``, which runs per-page and breaks
|
|
11
|
+
cross-page operations like sort/reverse - see GitHub CLI issue 10459).
|
|
14
12
|
"""
|
|
15
13
|
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
16
|
import argparse
|
|
17
17
|
import json
|
|
18
|
-
import subprocess
|
|
19
18
|
import sys
|
|
20
19
|
from pathlib import Path
|
|
21
20
|
|
|
@@ -26,14 +25,8 @@ from evict_cached_config_modules import evict_cached_config_modules
|
|
|
26
25
|
|
|
27
26
|
evict_cached_config_modules()
|
|
28
27
|
|
|
29
|
-
from
|
|
30
|
-
|
|
31
|
-
COPILOT_CLEAN_REVIEW_STATE,
|
|
32
|
-
COPILOT_REVIEWER_LOGIN,
|
|
33
|
-
COPILOT_SOFT_DIRTY_REVIEW_STATE,
|
|
34
|
-
GH_REVIEWS_PATH_TEMPLATE,
|
|
35
|
-
)
|
|
36
|
-
from review_field_helpers import body_of, login_of, state_of, submitted_at_of
|
|
28
|
+
from reviewer_fetch_core import fetch_reviewer_reviews
|
|
29
|
+
from reviewer_specs import copilot_spec
|
|
37
30
|
|
|
38
31
|
|
|
39
32
|
def fetch_copilot_reviews(
|
|
@@ -42,63 +35,10 @@ def fetch_copilot_reviews(
|
|
|
42
35
|
repo: str,
|
|
43
36
|
number: int,
|
|
44
37
|
) -> list[dict[str, object]]:
|
|
45
|
-
"""Return Copilot reviews newest-first, each with a
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"""
|
|
49
|
-
reviews_endpoint = GH_REVIEWS_PATH_TEMPLATE.format(
|
|
50
|
-
owner=owner, repo=repo, number=number
|
|
38
|
+
"""Return Copilot reviews newest-first, each with a classification."""
|
|
39
|
+
return fetch_reviewer_reviews(
|
|
40
|
+
copilot_spec, owner=owner, repo=repo, number=number
|
|
51
41
|
)
|
|
52
|
-
gh_command: list[str] = [
|
|
53
|
-
"gh",
|
|
54
|
-
"api",
|
|
55
|
-
reviews_endpoint,
|
|
56
|
-
"--paginate",
|
|
57
|
-
"--slurp",
|
|
58
|
-
]
|
|
59
|
-
completed = subprocess.run(
|
|
60
|
-
gh_command,
|
|
61
|
-
capture_output=True,
|
|
62
|
-
check=True,
|
|
63
|
-
text=True,
|
|
64
|
-
encoding="utf-8",
|
|
65
|
-
errors="replace",
|
|
66
|
-
)
|
|
67
|
-
pages: list[list[dict[str, object]]] = json.loads(completed.stdout)
|
|
68
|
-
all_flat_reviews = [each_review for each_page in pages for each_review in each_page]
|
|
69
|
-
all_copilot_reviews = [
|
|
70
|
-
each_review
|
|
71
|
-
for each_review in all_flat_reviews
|
|
72
|
-
if login_of(each_review) == COPILOT_REVIEWER_LOGIN
|
|
73
|
-
and each_review.get("submitted_at") is not None
|
|
74
|
-
and each_review.get("id") is not None
|
|
75
|
-
]
|
|
76
|
-
all_copilot_reviews.sort(
|
|
77
|
-
key=lambda each_review: submitted_at_of(each_review), reverse=True
|
|
78
|
-
)
|
|
79
|
-
return [
|
|
80
|
-
{
|
|
81
|
-
"review_id": each_review["id"],
|
|
82
|
-
"commit_id": each_review.get("commit_id"),
|
|
83
|
-
"submitted_at": each_review["submitted_at"],
|
|
84
|
-
"state": state_of(each_review),
|
|
85
|
-
"body": body_of(each_review),
|
|
86
|
-
"classification": _classify_review(each_review),
|
|
87
|
-
}
|
|
88
|
-
for each_review in all_copilot_reviews
|
|
89
|
-
]
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def _classify_review(field_by_key: dict[str, object]) -> str:
|
|
93
|
-
review_state = state_of(field_by_key)
|
|
94
|
-
if review_state == COPILOT_CLEAN_REVIEW_STATE:
|
|
95
|
-
return "clean"
|
|
96
|
-
if review_state not in ALL_COPILOT_DIRTY_REVIEW_STATES:
|
|
97
|
-
return "clean"
|
|
98
|
-
state_requires_body = review_state == COPILOT_SOFT_DIRTY_REVIEW_STATE
|
|
99
|
-
if state_requires_body and not body_of(field_by_key):
|
|
100
|
-
return "clean"
|
|
101
|
-
return "dirty"
|
|
102
42
|
|
|
103
43
|
|
|
104
44
|
def main() -> int:
|
|
@@ -9,16 +9,30 @@ Run: python3 packages/claude-dev-env/skills/pr-converge/scripts/reflow_skill_md.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
-
import
|
|
12
|
+
import sys
|
|
13
13
|
import textwrap
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
SKILL_PATH = Path(__file__).resolve().parent.parent / "SKILL.md"
|
|
16
|
+
script_directory = str(Path(__file__).resolve().parent)
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
while script_directory in sys.path:
|
|
19
|
+
sys.path.remove(script_directory)
|
|
20
|
+
if script_directory not in sys.path:
|
|
21
|
+
sys.path.insert(0, script_directory)
|
|
22
|
+
|
|
23
|
+
from evict_cached_config_modules import evict_cached_config_modules
|
|
24
|
+
|
|
25
|
+
evict_cached_config_modules()
|
|
26
|
+
|
|
27
|
+
from config.reflow_skill_md_constants import (
|
|
28
|
+
BASH_CONTINUATION_MARKER_WIDTH,
|
|
29
|
+
BULLET_LIST_ITEM_PATTERN as BULLET_RE,
|
|
30
|
+
MARKDOWN_REFERENCE_DEFINITION_PATTERN as REF_DEF_RE,
|
|
31
|
+
MAXIMUM_LINE_WIDTH as MAX_WIDTH,
|
|
32
|
+
ORDERED_LIST_ITEM_PATTERN as ORDERED_RE,
|
|
33
|
+
TARGET_SKILL_PATH as SKILL_PATH,
|
|
34
|
+
UNFINISHED_MARKDOWN_LINK_TARGET_PATTERN as UNFINISHED_MD_LINK_TARGET,
|
|
35
|
+
)
|
|
22
36
|
|
|
23
37
|
|
|
24
38
|
def wrap_paragraph_plain(text: str) -> list[str]:
|
|
@@ -49,11 +63,14 @@ def wrap_list_item(lead_ws: str, marker: str, body: str) -> list[str]:
|
|
|
49
63
|
).splitlines()
|
|
50
64
|
|
|
51
65
|
|
|
52
|
-
def reflow_yaml_description_block(
|
|
66
|
+
def reflow_yaml_description_block(
|
|
67
|
+
all_lines: list[str],
|
|
68
|
+
body_start: int,
|
|
69
|
+
) -> tuple[list[str], int]:
|
|
53
70
|
body_parts: list[str] = []
|
|
54
71
|
index = body_start
|
|
55
|
-
while index < len(
|
|
56
|
-
line =
|
|
72
|
+
while index < len(all_lines):
|
|
73
|
+
line = all_lines[index]
|
|
57
74
|
if line.strip() == "---":
|
|
58
75
|
index += 1
|
|
59
76
|
break
|
|
@@ -62,10 +79,6 @@ def reflow_yaml_description_block(lines: list[str], body_start: int) -> tuple[li
|
|
|
62
79
|
body_parts.append(stripped)
|
|
63
80
|
index += 1
|
|
64
81
|
merged = " ".join(body_parts)
|
|
65
|
-
merged = merged.replace(
|
|
66
|
-
"`<TMPDIR>/pr-converge-<session_id>/state.json` per",
|
|
67
|
-
"`<TMPDIR>/pr-converge-<session_id>/state.json>` per",
|
|
68
|
-
)
|
|
69
82
|
wrapped = textwrap.fill(
|
|
70
83
|
merged,
|
|
71
84
|
width=MAX_WIDTH,
|
|
@@ -96,6 +109,8 @@ def is_new_logical_line(stripped: str) -> bool:
|
|
|
96
109
|
return True
|
|
97
110
|
if ORDERED_RE.match(stripped) or BULLET_RE.match(stripped):
|
|
98
111
|
return True
|
|
112
|
+
if REF_DEF_RE.match(stripped):
|
|
113
|
+
return True
|
|
99
114
|
return False
|
|
100
115
|
|
|
101
116
|
|
|
@@ -110,30 +125,30 @@ def merge_without_space(buffer: str, continuation: str) -> bool:
|
|
|
110
125
|
return False
|
|
111
126
|
|
|
112
127
|
|
|
113
|
-
def merge_soft_breaks(
|
|
114
|
-
|
|
128
|
+
def merge_soft_breaks(all_lines: list[str]) -> list[str]:
|
|
129
|
+
reflowed_lines: list[str] = []
|
|
115
130
|
index = 0
|
|
116
|
-
|
|
117
|
-
while index < len(
|
|
118
|
-
raw =
|
|
131
|
+
is_inside_fence = False
|
|
132
|
+
while index < len(all_lines):
|
|
133
|
+
raw = all_lines[index]
|
|
119
134
|
line = raw.rstrip("\n")
|
|
120
135
|
if line.lstrip().startswith("```"):
|
|
121
|
-
|
|
122
|
-
|
|
136
|
+
is_inside_fence = not is_inside_fence
|
|
137
|
+
reflowed_lines.append(line)
|
|
123
138
|
index += 1
|
|
124
139
|
continue
|
|
125
|
-
if
|
|
126
|
-
|
|
140
|
+
if is_inside_fence:
|
|
141
|
+
reflowed_lines.append(line)
|
|
127
142
|
index += 1
|
|
128
143
|
continue
|
|
129
144
|
if line.strip() == "":
|
|
130
|
-
|
|
145
|
+
reflowed_lines.append(line)
|
|
131
146
|
index += 1
|
|
132
147
|
continue
|
|
133
148
|
buffer_line = line
|
|
134
149
|
index += 1
|
|
135
|
-
while index < len(
|
|
136
|
-
next_raw =
|
|
150
|
+
while index < len(all_lines):
|
|
151
|
+
next_raw = all_lines[index].rstrip("\n")
|
|
137
152
|
if next_raw.strip() == "":
|
|
138
153
|
break
|
|
139
154
|
if next_raw.lstrip().startswith("```"):
|
|
@@ -146,8 +161,8 @@ def merge_soft_breaks(lines: list[str]) -> list[str]:
|
|
|
146
161
|
else:
|
|
147
162
|
buffer_line = f"{buffer_line.rstrip()} {stripped_next}"
|
|
148
163
|
index += 1
|
|
149
|
-
|
|
150
|
-
return
|
|
164
|
+
reflowed_lines.append(buffer_line)
|
|
165
|
+
return reflowed_lines
|
|
151
166
|
|
|
152
167
|
|
|
153
168
|
def reflow_merged_line(line: str) -> list[str]:
|
|
@@ -191,6 +206,9 @@ def reflow_merged_line(line: str) -> list[str]:
|
|
|
191
206
|
break_on_hyphens=False,
|
|
192
207
|
).splitlines()
|
|
193
208
|
|
|
209
|
+
if REF_DEF_RE.match(stripped):
|
|
210
|
+
return [stripped]
|
|
211
|
+
|
|
194
212
|
ordered = ORDERED_RE.match(line)
|
|
195
213
|
if ordered:
|
|
196
214
|
return wrap_list_item(ordered.group(1), ordered.group(2), ordered.group(3))
|
|
@@ -202,39 +220,42 @@ def reflow_merged_line(line: str) -> list[str]:
|
|
|
202
220
|
return wrap_paragraph_plain(stripped)
|
|
203
221
|
|
|
204
222
|
|
|
205
|
-
def reflow_markdown_body(
|
|
206
|
-
merged = merge_soft_breaks(
|
|
207
|
-
|
|
223
|
+
def reflow_markdown_body(all_lines: list[str]) -> list[str]:
|
|
224
|
+
merged = merge_soft_breaks(all_lines)
|
|
225
|
+
reflowed_lines: list[str] = []
|
|
208
226
|
for each_line in merged:
|
|
209
227
|
if each_line.strip() == "":
|
|
210
|
-
|
|
228
|
+
reflowed_lines.append("")
|
|
211
229
|
continue
|
|
212
|
-
|
|
213
|
-
return
|
|
230
|
+
reflowed_lines.extend(reflow_merged_line(each_line))
|
|
231
|
+
return reflowed_lines
|
|
214
232
|
|
|
215
233
|
|
|
216
|
-
def wrap_long_bash_fence_lines(
|
|
234
|
+
def wrap_long_bash_fence_lines(all_lines: list[str]) -> list[str]:
|
|
217
235
|
"""Hard-wrap only ```bash fence bodies that still exceed MAX_WIDTH."""
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
for
|
|
221
|
-
stripped =
|
|
236
|
+
wrapped_lines: list[str] = []
|
|
237
|
+
is_inside_bash_fence = False
|
|
238
|
+
for each_line in all_lines:
|
|
239
|
+
stripped = each_line.lstrip()
|
|
222
240
|
if stripped.startswith("```"):
|
|
223
|
-
if not
|
|
241
|
+
if not is_inside_bash_fence:
|
|
224
242
|
lang = stripped[3:].strip().lower()
|
|
225
|
-
|
|
243
|
+
is_inside_bash_fence = lang == "bash"
|
|
226
244
|
else:
|
|
227
|
-
|
|
228
|
-
|
|
245
|
+
is_inside_bash_fence = False
|
|
246
|
+
wrapped_lines.append(each_line)
|
|
229
247
|
continue
|
|
230
|
-
if
|
|
231
|
-
indent_len = len(
|
|
232
|
-
indent =
|
|
233
|
-
|
|
248
|
+
if is_inside_bash_fence and len(each_line) > MAX_WIDTH:
|
|
249
|
+
indent_len = len(each_line) - len(each_line.lstrip())
|
|
250
|
+
indent = each_line[:indent_len]
|
|
251
|
+
if len(indent) + BASH_CONTINUATION_MARKER_WIDTH >= MAX_WIDTH:
|
|
252
|
+
wrapped_lines.append(each_line)
|
|
253
|
+
continue
|
|
254
|
+
content = each_line.lstrip()
|
|
234
255
|
wrapped_segments: list[str] = []
|
|
235
256
|
rest = content
|
|
236
257
|
while len(rest) > MAX_WIDTH - len(indent):
|
|
237
|
-
room = MAX_WIDTH - len(indent) -
|
|
258
|
+
room = MAX_WIDTH - len(indent) - BASH_CONTINUATION_MARKER_WIDTH
|
|
238
259
|
window = rest[:room]
|
|
239
260
|
break_at = window.rfind(" ")
|
|
240
261
|
if break_at <= 0:
|
|
@@ -244,10 +265,10 @@ def wrap_long_bash_fence_lines(lines: list[str]) -> list[str]:
|
|
|
244
265
|
wrapped_segments.append(indent + piece + " \\")
|
|
245
266
|
if rest:
|
|
246
267
|
wrapped_segments.append(indent + (" " if wrapped_segments else "") + rest)
|
|
247
|
-
|
|
268
|
+
wrapped_lines.extend(wrapped_segments)
|
|
248
269
|
else:
|
|
249
|
-
|
|
250
|
-
return
|
|
270
|
+
wrapped_lines.append(each_line)
|
|
271
|
+
return wrapped_lines
|
|
251
272
|
|
|
252
273
|
|
|
253
274
|
def main() -> None:
|
|
@@ -0,0 +1,153 @@
|
|
|
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
|
+
]
|