claude-dev-env 1.37.0 → 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.
Files changed (95) hide show
  1. package/CLAUDE.md +3 -0
  2. package/_shared/pr-loop/audit-contract.md +4 -3
  3. package/_shared/pr-loop/fix-protocol.md +2 -0
  4. package/_shared/pr-loop/gh-payloads.md +38 -37
  5. package/_shared/pr-loop/scripts/README.md +0 -1
  6. package/_shared/pr-loop/scripts/preflight.py +2 -1
  7. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +2 -2
  8. package/_shared/pr-loop/scripts/tests/test_preflight.py +22 -0
  9. package/_shared/pr-loop/state-schema.md +10 -10
  10. package/agents/clean-coder.md +4 -0
  11. package/agents/code-quality-agent.md +23 -85
  12. package/agents/groq-coder.md +8 -6
  13. package/hooks/blocking/__init__.py +0 -0
  14. package/hooks/blocking/hedging_language_blocker.py +2 -2
  15. package/hooks/blocking/state_description_blocker.py +243 -0
  16. package/hooks/blocking/tdd_enforcer.py +94 -0
  17. package/hooks/blocking/test_hedging_language_blocker.py +1 -1
  18. package/hooks/blocking/test_state_description_blocker.py +618 -0
  19. package/hooks/blocking/test_tdd_enforcer.py +152 -0
  20. package/hooks/config/state_description_blocker_constants.py +130 -0
  21. package/hooks/hooks.json +10 -0
  22. package/package.json +1 -1
  23. package/rules/gh-paginate.md +4 -50
  24. package/rules/no-historical-clutter.md +57 -0
  25. package/scripts/config/groq_bugteam_config.py +13 -5
  26. package/skills/bugteam/CONSTRAINTS.md +20 -27
  27. package/skills/bugteam/EXAMPLES.md +1 -1
  28. package/skills/bugteam/PROMPTS.md +78 -42
  29. package/skills/bugteam/SKILL.md +76 -63
  30. package/skills/bugteam/SKILL_EVALS.md +12 -12
  31. package/skills/bugteam/reference/audit-and-teammates.md +21 -48
  32. package/skills/bugteam/reference/audit-contract.md +7 -7
  33. package/skills/bugteam/reference/github-pr-reviews.md +31 -31
  34. package/skills/bugteam/reference/team-setup.md +1 -1
  35. package/skills/bugteam/reference/teardown-publish-permissions.md +4 -4
  36. package/skills/copilot-review/SKILL.md +7 -14
  37. package/skills/findbugs/SKILL.md +2 -2
  38. package/skills/fixbugs/SKILL.md +1 -1
  39. package/skills/monitor-open-prs/SKILL.md +6 -6
  40. package/skills/pr-converge/SKILL.md +7 -6
  41. package/skills/pr-converge/reference/convergence-gates.md +46 -44
  42. package/skills/pr-converge/reference/examples.md +4 -4
  43. package/skills/pr-converge/reference/fix-protocol.md +8 -8
  44. package/skills/pr-converge/reference/multi-pr-orchestration.md +10 -10
  45. package/skills/pr-converge/reference/per-tick.md +24 -36
  46. package/skills/pr-converge/reference/stop-conditions.md +7 -7
  47. package/skills/pr-converge/scripts/README.md +65 -117
  48. package/skills/pr-review-responder/EXAMPLES.md +2 -2
  49. package/skills/pr-review-responder/PRINCIPLES.md +2 -8
  50. package/skills/pr-review-responder/README.md +7 -48
  51. package/skills/pr-review-responder/SKILL.md +2 -3
  52. package/skills/pr-review-responder/TESTING.md +8 -65
  53. package/skills/qbug/SKILL.md +10 -16
  54. package/_shared/pr-loop/scripts/config/gh_util_constants.py +0 -31
  55. package/_shared/pr-loop/scripts/gh_util.py +0 -193
  56. package/_shared/pr-loop/scripts/tests/test_gh_util.py +0 -257
  57. package/_shared/pr-loop/scripts/tests/test_gh_util_constants.py +0 -61
  58. package/skills/pr-converge/scripts/check_pr_mergeability.py +0 -78
  59. package/skills/pr-converge/scripts/config/pr_converge_constants.py +0 -118
  60. package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +0 -152
  61. package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +0 -70
  62. package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +0 -57
  63. package/skills/pr-converge/scripts/fetch_claude_inline_comments.py +0 -70
  64. package/skills/pr-converge/scripts/fetch_claude_reviews.py +0 -61
  65. package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +0 -70
  66. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +0 -61
  67. package/skills/pr-converge/scripts/mark_pr_ready.py +0 -54
  68. package/skills/pr-converge/scripts/post-bugbot-run.helpers.ps1 +0 -49
  69. package/skills/pr-converge/scripts/post-bugbot-run.ps1 +0 -33
  70. package/skills/pr-converge/scripts/reply_to_inline_comment.py +0 -84
  71. package/skills/pr-converge/scripts/request_copilot_review.py +0 -71
  72. package/skills/pr-converge/scripts/resolve_pr_head.py +0 -58
  73. package/skills/pr-converge/scripts/review_field_helpers.py +0 -43
  74. package/skills/pr-converge/scripts/reviewer_fetch_core.py +0 -153
  75. package/skills/pr-converge/scripts/reviewer_specs.py +0 -98
  76. package/skills/pr-converge/scripts/test_check_pr_mergeability.py +0 -126
  77. package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +0 -443
  78. package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +0 -299
  79. package/skills/pr-converge/scripts/test_fetch_claude_inline_comments.py +0 -485
  80. package/skills/pr-converge/scripts/test_fetch_claude_reviews.py +0 -368
  81. package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +0 -440
  82. package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +0 -366
  83. package/skills/pr-converge/scripts/test_mark_pr_ready.py +0 -69
  84. package/skills/pr-converge/scripts/test_post_bugbot_run.py +0 -195
  85. package/skills/pr-converge/scripts/test_reply_to_inline_comment.py +0 -159
  86. package/skills/pr-converge/scripts/test_request_copilot_review.py +0 -101
  87. package/skills/pr-converge/scripts/test_resolve_pr_head.py +0 -79
  88. package/skills/pr-converge/scripts/test_review_field_helpers.py +0 -80
  89. package/skills/pr-converge/scripts/test_reviewer_fetch_core.py +0 -448
  90. package/skills/pr-converge/scripts/test_reviewer_specs.py +0 -107
  91. package/skills/pr-converge/scripts/test_trigger_bugbot.py +0 -139
  92. package/skills/pr-converge/scripts/test_view_pr_context.py +0 -111
  93. package/skills/pr-converge/scripts/trigger_bugbot.py +0 -77
  94. package/skills/pr-converge/scripts/view_pr_context.py +0 -47
  95. package/skills/pr-review-responder/scripts/respond_to_reviews.py +0 -376
@@ -1,111 +0,0 @@
1
- """Tests for view_pr_context.
2
-
3
- Covers:
4
- - gh pr view is invoked with the documented --json field list
5
- - the parsed JSON object is returned
6
- - subprocess errors propagate
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import importlib.util
12
- import json
13
- import subprocess
14
- from pathlib import Path
15
- from types import ModuleType
16
- from unittest.mock import MagicMock, patch
17
-
18
- import pytest
19
-
20
-
21
- def _load_module() -> ModuleType:
22
- module_path = Path(__file__).parent / "view_pr_context.py"
23
- spec = importlib.util.spec_from_file_location("view_pr_context", module_path)
24
- assert spec is not None
25
- assert spec.loader is not None
26
- module = importlib.util.module_from_spec(spec)
27
- spec.loader.exec_module(module)
28
- return module
29
-
30
-
31
- view_pr_context_module = _load_module()
32
-
33
-
34
- def _completed(stdout: str) -> subprocess.CompletedProcess:
35
- process = MagicMock(spec=subprocess.CompletedProcess)
36
- process.stdout = stdout
37
- process.returncode = 0
38
- return process
39
-
40
-
41
- def test_should_invoke_gh_pr_view_with_documented_field_list() -> None:
42
- payload = json.dumps(
43
- {
44
- "number": 42,
45
- "url": "https://github.com/acme/widget/pull/42",
46
- "headRefOid": "abc123",
47
- "baseRefName": "main",
48
- "headRefName": "feat/x",
49
- "isDraft": True,
50
- }
51
- )
52
- with patch("subprocess.run") as mock_run:
53
- mock_run.return_value = _completed(payload)
54
- view_pr_context_module.view_pr_context()
55
- invoked_argv = mock_run.call_args[0][0]
56
- assert invoked_argv[0:3] == ["gh", "pr", "view"]
57
- assert "--json" in invoked_argv
58
- fields_arg = invoked_argv[invoked_argv.index("--json") + 1]
59
- for required_field in (
60
- "number",
61
- "url",
62
- "headRefOid",
63
- "baseRefName",
64
- "headRefName",
65
- "isDraft",
66
- ):
67
- assert required_field in fields_arg
68
-
69
-
70
- def test_should_return_parsed_json_object() -> None:
71
- payload = {
72
- "number": 42,
73
- "url": "https://github.com/acme/widget/pull/42",
74
- "headRefOid": "abc123",
75
- "baseRefName": "main",
76
- "headRefName": "feat/x",
77
- "isDraft": True,
78
- }
79
- with patch("subprocess.run") as mock_run:
80
- mock_run.return_value = _completed(json.dumps(payload))
81
- pr_context = view_pr_context_module.view_pr_context()
82
- assert pr_context == payload
83
-
84
-
85
- def test_should_raise_when_gh_subprocess_fails() -> None:
86
- failure = subprocess.CalledProcessError(
87
- returncode=1, cmd=["gh"], stderr="auth failure"
88
- )
89
- with patch("subprocess.run", side_effect=failure):
90
- with pytest.raises(subprocess.CalledProcessError):
91
- view_pr_context_module.view_pr_context()
92
-
93
-
94
- def test_should_pass_imported_constant_directly_without_local_alias() -> None:
95
- payload = json.dumps(
96
- {
97
- "number": 7,
98
- "url": "https://github.com/acme/widget/pull/7",
99
- "headRefOid": "deadbeef",
100
- "baseRefName": "main",
101
- "headRefName": "feat/y",
102
- "isDraft": False,
103
- }
104
- )
105
- with patch("subprocess.run") as mock_run:
106
- mock_run.return_value = _completed(payload)
107
- view_pr_context_module.view_pr_context()
108
- invoked_argv = mock_run.call_args[0][0]
109
- fields_arg = invoked_argv[invoked_argv.index("--json") + 1]
110
- expected_fields = view_pr_context_module.PR_CONTEXT_FIELDS
111
- assert fields_arg is expected_fields
@@ -1,77 +0,0 @@
1
- """Post a `bugbot run` comment to re-trigger a Cursor Bugbot review.
2
-
3
- Writes the literal trigger phrase to a temp file (per the gh-body-file rule —
4
- `gh pr comment --body "..."` may corrupt backticks), invokes
5
- `gh pr comment --body-file`, and removes the temp file on success or failure.
6
- """
7
-
8
- import argparse
9
- import os
10
- import subprocess
11
- import sys
12
- import tempfile
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
- BUGBOT_RUN_TEMPFILE_PREFIX,
24
- BUGBOT_RUN_TEMPFILE_SUFFIX,
25
- BUGBOT_RUN_TRIGGER_PHRASE,
26
- GH_REPO_ARG_TEMPLATE,
27
- )
28
-
29
-
30
- def trigger_bugbot(*, owner: str, repo: str, number: int) -> str:
31
- """Post the bugbot re-trigger comment, return the comment URL gh emits."""
32
- file_descriptor, raw_path = tempfile.mkstemp(
33
- suffix=BUGBOT_RUN_TEMPFILE_SUFFIX, prefix=BUGBOT_RUN_TEMPFILE_PREFIX
34
- )
35
- try:
36
- os.close(file_descriptor)
37
- body_file_path = Path(raw_path)
38
- body_file_path.write_text(BUGBOT_RUN_TRIGGER_PHRASE, encoding="utf-8")
39
- repo_arg = GH_REPO_ARG_TEMPLATE.format(owner=owner, repo=repo)
40
- gh_command: list[str] = [
41
- "gh",
42
- "pr",
43
- "comment",
44
- str(number),
45
- "--repo",
46
- repo_arg,
47
- "--body-file",
48
- str(body_file_path),
49
- ]
50
- completed = subprocess.run(
51
- gh_command,
52
- capture_output=True,
53
- check=True,
54
- text=True,
55
- encoding="utf-8",
56
- errors="replace",
57
- )
58
- return completed.stdout.strip()
59
- finally:
60
- Path(raw_path).unlink(missing_ok=True)
61
-
62
-
63
- def main() -> int:
64
- parser = argparse.ArgumentParser(description=__doc__)
65
- parser.add_argument("--owner", required=True)
66
- parser.add_argument("--repo", required=True)
67
- parser.add_argument("--number", required=True, type=int)
68
- parsed_arguments = parser.parse_args()
69
- comment_url = trigger_bugbot(
70
- owner=parsed_arguments.owner, repo=parsed_arguments.repo, number=parsed_arguments.number
71
- )
72
- sys.stdout.write(f"{comment_url}\n")
73
- return 0
74
-
75
-
76
- if __name__ == "__main__":
77
- sys.exit(main())
@@ -1,47 +0,0 @@
1
- """Resolve the per-tick PR context (number, url, head sha, branch names, draft state).
2
-
3
- Wraps `gh pr view --json ...` so the skill body emits one script invocation
4
- instead of repeating the field list inline.
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 PR_CONTEXT_FIELDS
21
-
22
-
23
- def view_pr_context() -> dict[str, object]:
24
- """Return the parsed JSON object from `gh pr view --json <fields>`."""
25
- gh_command: list[str] = ["gh", "pr", "view", "--json", PR_CONTEXT_FIELDS]
26
- completed = subprocess.run(
27
- gh_command,
28
- capture_output=True,
29
- check=True,
30
- text=True,
31
- encoding="utf-8",
32
- errors="replace",
33
- )
34
- return json.loads(completed.stdout)
35
-
36
-
37
- def main() -> int:
38
- parser = argparse.ArgumentParser(description=__doc__)
39
- parser.parse_args()
40
- pr_context = view_pr_context()
41
- json.dump(pr_context, sys.stdout)
42
- sys.stdout.write("\n")
43
- return 0
44
-
45
-
46
- if __name__ == "__main__":
47
- sys.exit(main())
@@ -1,376 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Standalone script to respond to GitHub PR review comments.
4
-
5
- Usage:
6
- python respond_to_reviews.py [--pr PR_NUMBER] [--auto-approve]
7
-
8
- Requirements:
9
- - gh CLI installed and authenticated
10
- - Git repository with GitHub remote
11
- - Python 3.8+
12
- """
13
-
14
- import json
15
- import subprocess
16
- import sys
17
- from dataclasses import dataclass
18
- from pathlib import Path
19
- from typing import Dict, List, Optional, Set
20
-
21
-
22
- @dataclass
23
- class ReviewComment:
24
- id: int
25
- path: str
26
- line: int
27
- body: str
28
- user: str
29
- created_at: str
30
- in_reply_to: Optional[int]
31
-
32
-
33
- @dataclass
34
- class FileChange:
35
- path: str
36
- lines_changed: Set[int]
37
- diff: str
38
-
39
-
40
- def run_command(cmd: List[str]) -> str:
41
- """Run shell command and return output."""
42
- try:
43
- result = subprocess.run(
44
- cmd,
45
- capture_output=True,
46
- text=True,
47
- check=True
48
- )
49
- return result.stdout.strip()
50
- except subprocess.CalledProcessError as e:
51
- print(f"Error running command: {' '.join(cmd)}", file=sys.stderr)
52
- print(f"Error: {e.stderr}", file=sys.stderr)
53
- sys.exit(1)
54
-
55
-
56
- def get_current_pr() -> Optional[Dict]:
57
- """Get PR number for current branch."""
58
- output = run_command(['gh', 'pr', 'view', '--json', 'number,title,url'])
59
- if not output:
60
- return None
61
- return json.loads(output)
62
-
63
-
64
- def get_review_comments(pr_number: int, repo: str) -> List[ReviewComment]:
65
- """Fetch all review comments from PR."""
66
- cmd = [
67
- 'gh', 'api',
68
- f'repos/{repo}/pulls/{pr_number}/comments',
69
- '--jq',
70
- '.[] | {id, path, line, body, user: .user.login, created_at, in_reply_to}'
71
- ]
72
- output = run_command(cmd)
73
-
74
- comments = []
75
- for line in output.split('\n'):
76
- if not line:
77
- continue
78
- data = json.loads(line)
79
- comments.append(ReviewComment(
80
- id=data['id'],
81
- path=data['path'],
82
- line=data['line'],
83
- body=data['body'],
84
- user=data['user'],
85
- created_at=data['created_at'],
86
- in_reply_to=data.get('in_reply_to')
87
- ))
88
-
89
- return comments
90
-
91
-
92
- def get_current_user() -> str:
93
- """Get current GitHub username."""
94
- return run_command(['gh', 'api', 'user', '--jq', '.login'])
95
-
96
-
97
- def filter_unresponded_comments(
98
- comments: List[ReviewComment],
99
- current_user: str
100
- ) -> List[ReviewComment]:
101
- """Filter for comments that haven't been responded to."""
102
- # Group comments by thread (in_reply_to chain)
103
- threads: Dict[int, List[ReviewComment]] = {}
104
-
105
- for comment in comments:
106
- if comment.in_reply_to is None:
107
- # Top-level comment
108
- thread_id = comment.id
109
- else:
110
- # Reply to another comment
111
- thread_id = comment.in_reply_to
112
-
113
- if thread_id not in threads:
114
- threads[thread_id] = []
115
- threads[thread_id].append(comment)
116
-
117
- # Find threads where we haven't replied
118
- unresponded = []
119
- for thread_id, thread_comments in threads.items():
120
- # Check if current user has replied in this thread
121
- user_replied = any(c.user == current_user for c in thread_comments)
122
-
123
- if not user_replied:
124
- # Find the original comment (first in thread)
125
- original = min(thread_comments, key=lambda c: c.created_at)
126
- if original.user != current_user:
127
- unresponded.append(original)
128
-
129
- return unresponded
130
-
131
-
132
- def get_changed_files() -> List[FileChange]:
133
- """Get files changed in the last commit."""
134
- # Get list of changed files
135
- files_output = run_command(['git', 'diff', '--name-only', 'HEAD~1..HEAD'])
136
-
137
- changes = []
138
- for file_path in files_output.split('\n'):
139
- if not file_path:
140
- continue
141
-
142
- # Get diff for this file
143
- diff = run_command(['git', 'diff', 'HEAD~1..HEAD', '--', file_path])
144
-
145
- # Parse changed line numbers from diff
146
- lines_changed = set()
147
- for line in diff.split('\n'):
148
- if line.startswith('@@'):
149
- # Parse @@ -old_start,old_count +new_start,new_count @@
150
- parts = line.split(' ')
151
- if len(parts) >= 3:
152
- new_range = parts[2] # +new_start,new_count
153
- if ',' in new_range:
154
- start, count = new_range[1:].split(',')
155
- start_line = int(start)
156
- count_lines = int(count)
157
- lines_changed.update(range(start_line, start_line + count_lines))
158
-
159
- changes.append(FileChange(
160
- path=file_path,
161
- lines_changed=lines_changed,
162
- diff=diff
163
- ))
164
-
165
- return changes
166
-
167
-
168
- def match_comments_to_changes(
169
- comments: List[ReviewComment],
170
- changes: List[FileChange]
171
- ) -> List[tuple[ReviewComment, FileChange]]:
172
- """Match review comments to file changes."""
173
- matches = []
174
-
175
- changes_by_path = {c.path: c for c in changes}
176
-
177
- for comment in comments:
178
- if comment.path in changes_by_path:
179
- change = changes_by_path[comment.path]
180
- # Check if the commented line was changed
181
- if comment.line in change.lines_changed or not change.lines_changed:
182
- # Either the exact line changed, or we changed the file (good enough)
183
- matches.append((comment, change))
184
-
185
- return matches
186
-
187
-
188
- def draft_response(comment: ReviewComment, change: FileChange) -> str:
189
- """Draft a concise response to a review comment."""
190
- # Analyze the diff to understand what changed
191
- diff_lines = change.diff.split('\n')
192
-
193
- # Look for common patterns
194
- if 'class ' in change.diff and '- class ' in change.diff:
195
- return "Removed wrapper class, using direct approach"
196
-
197
- if 'def ' in change.diff:
198
- if '+ def ' in change.diff:
199
- return "Extracted to shared function"
200
- if 'Type[' in change.diff or ': ' in change.diff:
201
- return "Added type hints"
202
-
203
- if 'import ' in change.diff:
204
- return "Updated imports"
205
-
206
- if '.css' in comment.path or 'style' in change.diff:
207
- return "Moved CSS values to stylesheet"
208
-
209
- if 'select_related' in change.diff or 'prefetch_related' in change.diff:
210
- return "Added query optimization to eliminate N+1"
211
-
212
- if comment.path.endswith('.py'):
213
- # Generic Python change
214
- return f"Updated {Path(comment.path).name}"
215
-
216
- # Generic fallback
217
- return f"Addressed feedback in {comment.path}"
218
-
219
-
220
- def post_response(comment_id: int, response: str, repo: str) -> bool:
221
- """Post response to GitHub review comment."""
222
- formatted_response = f"✅ **Fixed**: {response}"
223
-
224
- try:
225
- run_command([
226
- 'gh', 'api',
227
- f'repos/{repo}/pulls/comments/{comment_id}/replies',
228
- '-X', 'POST',
229
- '-f', f'body={formatted_response}'
230
- ])
231
- return True
232
- except Exception as e:
233
- print(f"Failed to post response: {e}", file=sys.stderr)
234
- return False
235
-
236
-
237
- def get_repo_name() -> str:
238
- """Get owner/repo from git remote."""
239
- remote_url = run_command(['git', 'remote', 'get-url', 'origin'])
240
-
241
- # Parse GitHub URL
242
- # SSH: git@github.com:owner/repo.git
243
- # HTTPS: https://github.com/owner/repo.git
244
-
245
- if 'github.com' not in remote_url:
246
- print("Error: Not a GitHub repository", file=sys.stderr)
247
- sys.exit(1)
248
-
249
- if remote_url.startswith('git@'):
250
- # SSH format
251
- repo_part = remote_url.split(':')[1]
252
- else:
253
- # HTTPS format
254
- repo_part = '/'.join(remote_url.split('/')[-2:])
255
-
256
- # Remove .git suffix
257
- return repo_part.replace('.git', '')
258
-
259
-
260
- def main():
261
- import argparse
262
-
263
- parser = argparse.ArgumentParser(
264
- description='Respond to GitHub PR review comments'
265
- )
266
- parser.add_argument(
267
- '--pr',
268
- type=int,
269
- help='PR number (auto-detected if not provided)'
270
- )
271
- parser.add_argument(
272
- '--auto-approve',
273
- action='store_true',
274
- help='Auto-approve all responses without confirmation'
275
- )
276
-
277
- args = parser.parse_args()
278
-
279
- # Get PR number
280
- if args.pr:
281
- pr_number = args.pr
282
- pr_title = f"PR #{pr_number}"
283
- pr_url = ""
284
- else:
285
- pr = get_current_pr()
286
- if not pr:
287
- print("Error: No PR found for current branch", file=sys.stderr)
288
- print("Create a PR first or specify --pr NUMBER", file=sys.stderr)
289
- sys.exit(1)
290
- pr_number = pr['number']
291
- pr_title = pr['title']
292
- pr_url = pr['url']
293
-
294
- print(f"Checking PR #{pr_number}: {pr_title}")
295
- if pr_url:
296
- print(f"URL: {pr_url}")
297
- print()
298
-
299
- # Get repository name
300
- repo = get_repo_name()
301
-
302
- # Get current user
303
- current_user = get_current_user()
304
-
305
- # Fetch review comments
306
- print("Fetching review comments...")
307
- all_comments = get_review_comments(pr_number, repo)
308
- print(f"Found {len(all_comments)} total review comments")
309
-
310
- # Filter for unresponded comments
311
- unresponded = filter_unresponded_comments(all_comments, current_user)
312
- print(f"Found {len(unresponded)} unresponded comments")
313
-
314
- if not unresponded:
315
- print("\nNo unresponded comments found!")
316
- return
317
-
318
- # Get changed files
319
- print("\nAnalyzing recent changes...")
320
- changes = get_changed_files()
321
- print(f"Found {len(changes)} changed files in last commit")
322
-
323
- # Match comments to changes
324
- matches = match_comments_to_changes(unresponded, changes)
325
-
326
- if not matches:
327
- print("\nNo review comments match your recent changes.")
328
- print(f"\nReview comments are about:")
329
- for comment in unresponded:
330
- print(f" - {comment.path}:{comment.line}")
331
- print(f"\nBut you changed:")
332
- for change in changes:
333
- print(f" - {change.path}")
334
- return
335
-
336
- # Draft responses
337
- print(f"\nFound {len(matches)} review comments addressed:\n")
338
-
339
- responses = []
340
- for i, (comment, change) in enumerate(matches, 1):
341
- response = draft_response(comment, change)
342
- responses.append((comment, response))
343
-
344
- print(f"{i}. @{comment.user} on {comment.path}:{comment.line}")
345
- print(f" Comment: {comment.body[:80]}...")
346
- print(f" Response: ✅ **Fixed**: {response}")
347
- print()
348
-
349
- # Get approval
350
- if not args.auto_approve:
351
- answer = input(f"Post these {len(responses)} responses to the PR? (y/n) ")
352
- if answer.lower() != 'y':
353
- print("Cancelled.")
354
- return
355
-
356
- # Post responses
357
- print("\nPosting responses...")
358
- success_count = 0
359
-
360
- for comment, response in responses:
361
- if post_response(comment.id, response, repo):
362
- print(f" ✓ {comment.path}:{comment.line}")
363
- success_count += 1
364
- else:
365
- print(f" ✗ {comment.path}:{comment.line}")
366
-
367
- print(f"\nPosted {success_count}/{len(responses)} responses to PR #{pr_number}")
368
-
369
- if pr_url:
370
- print(f"View PR: {pr_url}")
371
-
372
- print("\nReady to push!")
373
-
374
-
375
- if __name__ == '__main__':
376
- main()