claude-dev-env 1.0.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/LICENSE +21 -0
- package/README.md +219 -0
- package/agents/agent-writer.md +157 -0
- package/agents/clasp-deployment-orchestrator.md +609 -0
- package/agents/clean-coder.md +295 -0
- package/agents/code-quality-agent.md +40 -0
- package/agents/code-standards-agent.md +93 -0
- package/agents/config-centralizer.md +686 -0
- package/agents/config-extraction-agent.md +225 -0
- package/agents/doc-orchestrator.md +47 -0
- package/agents/docs-agent.md +112 -0
- package/agents/docx-agent.md +211 -0
- package/agents/git-commit-crafter.md +100 -0
- package/agents/magic-value-eliminator-agent.md +72 -0
- package/agents/mandatory-agent-workflow-agent.md +88 -0
- package/agents/parallel-workflow-coordinator.md +779 -0
- package/agents/pdf-agent.md +302 -0
- package/agents/plan-executor.md +226 -0
- package/agents/pr-description-writer.md +87 -0
- package/agents/project-context-loader.md +238 -0
- package/agents/project-docs-analyzer.md +54 -0
- package/agents/project-structure-organizer-agent.md +72 -0
- package/agents/readability-review-agent.md +76 -0
- package/agents/refactoring-specialist.md +69 -0
- package/agents/right-sized-engineer.md +129 -0
- package/agents/session-continuity-manager.md +53 -0
- package/agents/skill-to-agent-converter.md +371 -0
- package/agents/skill-writer-agent.md +470 -0
- package/agents/stub-detector-agent.md +140 -0
- package/agents/tdd-test-writer.md +62 -0
- package/agents/test-data-builder.md +68 -0
- package/agents/tooling-builder.md +78 -0
- package/agents/user-docs-writer.md +67 -0
- package/agents/validation-expert.md +71 -0
- package/agents/workflow-visual-documenter.md +82 -0
- package/agents/xlsx-agent.md +169 -0
- package/bin/install.mjs +256 -0
- package/commands/commit.md +28 -0
- package/commands/docupdate.md +322 -0
- package/commands/implement.md +102 -0
- package/commands/initialize.md +91 -0
- package/commands/plan.md +63 -0
- package/commands/pr-comments.md +47 -0
- package/commands/readability-review.md +20 -0
- package/commands/review-plan.md +7 -0
- package/commands/right-size.md +15 -0
- package/commands/stubcheck.md +89 -0
- package/commands/sum.md +30 -0
- package/docs/CODE_RULES.md +186 -0
- package/docs/DJANGO_PATTERNS.md +80 -0
- package/docs/REACT_PATTERNS.md +185 -0
- package/docs/TEST_QUALITY.md +104 -0
- package/hooks/advisory/migration-safety-advisor.py +49 -0
- package/hooks/advisory/refactor-guard.py +205 -0
- package/hooks/blocking/block-main-commit.py +168 -0
- package/hooks/blocking/code-rules-enforcer.py +549 -0
- package/hooks/blocking/destructive-command-blocker.py +107 -0
- package/hooks/blocking/docker-settings-guard.py +44 -0
- package/hooks/blocking/hedging-language-blocker.py +130 -0
- package/hooks/blocking/parallel-task-blocker.py +69 -0
- package/hooks/blocking/pr-description-enforcer.py +87 -0
- package/hooks/blocking/pyautogui-scroll-blocker.py +74 -0
- package/hooks/blocking/sensitive-file-protector.py +70 -0
- package/hooks/blocking/tdd-enforcer.py +62 -0
- package/hooks/blocking/test-preflight-check.py +343 -0
- package/hooks/blocking/write-existing-file-blocker.py +63 -0
- package/hooks/git-hooks/post-commit.py +103 -0
- package/hooks/github-action/test_workflow.py +33 -0
- package/hooks/hooks.json +246 -0
- package/hooks/lifecycle/config-change-guard.py +84 -0
- package/hooks/lifecycle/session-end-cleanup.py +59 -0
- package/hooks/notification/attention-needed-notify.py +63 -0
- package/hooks/notification/claude-notification-handler.py +59 -0
- package/hooks/notification/notification_utils.py +206 -0
- package/hooks/rewrite-plugin-paths.py +116 -0
- package/hooks/session/bulk-edit-reminder.py +30 -0
- package/hooks/session/code-rules-reminder.py +97 -0
- package/hooks/session/compact-context-reinject.py +39 -0
- package/hooks/session/hook-structure-context.py +140 -0
- package/hooks/session/plugin-data-dir-cleanup.py +39 -0
- package/hooks/validation/code-style-validator.py +145 -0
- package/hooks/validation/e2e-test-validator.py +142 -0
- package/hooks/validation/hook-format-validator.py +66 -0
- package/hooks/validation/mypy_validator.py +180 -0
- package/hooks/validators/README.md +125 -0
- package/hooks/validators/VALIDATION_REPORT.md +287 -0
- package/hooks/validators/__init__.py +19 -0
- package/hooks/validators/abbreviation_checks.py +82 -0
- package/hooks/validators/code_quality_checks.py +133 -0
- package/hooks/validators/comment_checks.py +188 -0
- package/hooks/validators/file_structure_checks.py +182 -0
- package/hooks/validators/git_checks.py +107 -0
- package/hooks/validators/health_check.py +214 -0
- package/hooks/validators/magic_value_checks.py +81 -0
- package/hooks/validators/mypy_integration.py +52 -0
- package/hooks/validators/output_formatter.py +266 -0
- package/hooks/validators/pr_reference_checks.py +72 -0
- package/hooks/validators/python_antipattern_checks.py +110 -0
- package/hooks/validators/python_style_checks.py +364 -0
- package/hooks/validators/react_checks.py +90 -0
- package/hooks/validators/ruff_integration.py +80 -0
- package/hooks/validators/run_all_validators.py +772 -0
- package/hooks/validators/security_checks.py +135 -0
- package/hooks/validators/test_abbreviation_checks.py +76 -0
- package/hooks/validators/test_bad.tsx +7 -0
- package/hooks/validators/test_code_quality_checks.py +129 -0
- package/hooks/validators/test_file_structure_checks.py +307 -0
- package/hooks/validators/test_files/01_basic_component.tsx +10 -0
- package/hooks/validators/test_files/02_component_without_react.tsx +10 -0
- package/hooks/validators/test_files/03_pure_component.tsx +10 -0
- package/hooks/validators/test_files/04_pure_component_import.tsx +10 -0
- package/hooks/validators/test_files/05_typescript_generics.tsx +14 -0
- package/hooks/validators/test_files/06_typescript_two_generics.tsx +18 -0
- package/hooks/validators/test_files/07_multiline_declaration.tsx +11 -0
- package/hooks/validators/test_files/08_error_boundary_valid.tsx +14 -0
- package/hooks/validators/test_files/09_error_boundary_with_other_class.tsx +20 -0
- package/hooks/validators/test_files/10_inheritance_chain.tsx +16 -0
- package/hooks/validators/test_files/11_ts_file.ts +10 -0
- package/hooks/validators/test_files/12_non_react_class.tsx +14 -0
- package/hooks/validators/test_files/13_functional_component.tsx +8 -0
- package/hooks/validators/test_files/14_indented_class.tsx +13 -0
- package/hooks/validators/test_files/15_getDerivedStateFromError.tsx +14 -0
- package/hooks/validators/test_files/16_mixed_components.tsx +20 -0
- package/hooks/validators/test_files/EXECUTIVE_SUMMARY.md +175 -0
- package/hooks/validators/test_files/TEST_RESULTS_TABLE.txt +60 -0
- package/hooks/validators/test_files/VALIDATION_REPORT.md +201 -0
- package/hooks/validators/test_files/async_views.py +23 -0
- package/hooks/validators/test_files/async_with_imports.py +14 -0
- package/hooks/validators/test_files/bad_inline_imports.py +37 -0
- package/hooks/validators/test_files/management/commands/cmd_01_no_debug_check.py +10 -0
- package/hooks/validators/test_files/management/commands/cmd_02_proper_debug_check.py +14 -0
- package/hooks/validators/test_files/management/commands/cmd_03_debug_check_with_return.py +14 -0
- package/hooks/validators/test_files/management/commands/cmd_04_imported_DEBUG.py +14 -0
- package/hooks/validators/test_files/management/commands/cmd_05_debug_check_in_helper.py +16 -0
- package/hooks/validators/test_files/management/commands/cmd_06_debug_check_late.py +22 -0
- package/hooks/validators/test_files/management/commands/cmd_07_positive_debug_check.py +15 -0
- package/hooks/validators/test_files/management/commands/cmd_08_debug_with_and.py +14 -0
- package/hooks/validators/test_files/not_management_command.py +10 -0
- package/hooks/validators/test_files/skip_decorators/test_01_simple_skip.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_02_pytest_skipif.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_03_unittest_skipIf.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_04_skip_with_parens.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_05_xfail.py +7 -0
- package/hooks/validators/test_files/skip_decorators/test_06_custom_skip.py +11 -0
- package/hooks/validators/test_files/skip_decorators/test_07_capital_Skip.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_08_skipUnless.py +7 -0
- package/hooks/validators/test_files/skip_decorators/test_09_pytest_mark_skip_simple.py +7 -0
- package/hooks/validators/test_files/test_async_functions.py +45 -0
- package/hooks/validators/test_files/test_purecomponent/PureComponentExample.tsx +7 -0
- package/hooks/validators/test_files/test_purecomponent/ReactPureComponentExample.tsx +7 -0
- package/hooks/validators/test_git_checks.py +295 -0
- package/hooks/validators/test_good.tsx +5 -0
- package/hooks/validators/test_health_check.py +57 -0
- package/hooks/validators/test_magic_value_checks.py +63 -0
- package/hooks/validators/test_mypy_integration.py +27 -0
- package/hooks/validators/test_output_formatter.py +150 -0
- package/hooks/validators/test_pr_reference_checks.py +41 -0
- package/hooks/validators/test_python_antipattern_checks.py +113 -0
- package/hooks/validators/test_python_style_checks.py +439 -0
- package/hooks/validators/test_react_checks.py +213 -0
- package/hooks/validators/test_results.txt +25 -0
- package/hooks/validators/test_ruff_integration.py +27 -0
- package/hooks/validators/test_run_all_validators.py +228 -0
- package/hooks/validators/test_run_all_validators_integration.py +48 -0
- package/hooks/validators/test_safety_checks.py +243 -0
- package/hooks/validators/test_security_checks.py +105 -0
- package/hooks/validators/test_test_safety_checks.py +321 -0
- package/hooks/validators/test_todo_checks.py +39 -0
- package/hooks/validators/test_type_safety_checks.py +85 -0
- package/hooks/validators/test_useless_test_checks.py +55 -0
- package/hooks/validators/test_validator_base.py +26 -0
- package/hooks/validators/test_verify_paths.py +34 -0
- package/hooks/validators/todo_checks.py +59 -0
- package/hooks/validators/type_safety_checks.py +101 -0
- package/hooks/validators/useless_test_checks.py +92 -0
- package/hooks/validators/validator_base.py +19 -0
- package/hooks/validators/verify_paths.py +57 -0
- package/hooks/workflow/auto-formatter.py +114 -0
- package/hooks/workflow/investigation-tracker-reset.py +46 -0
- package/package.json +30 -0
- package/rules/agent-spawn-protocol.md +47 -0
- package/rules/cleanup-temp-files.md +27 -0
- package/rules/code-reviews.md +11 -0
- package/rules/code-standards.md +43 -0
- package/rules/conservative-action.md +20 -0
- package/rules/context7.md +12 -0
- package/rules/explore-thoroughly.md +27 -0
- package/rules/git-workflow.md +42 -0
- package/rules/parallel-tools.md +23 -0
- package/rules/research-mode.md +23 -0
- package/rules/right-sized-engineering.md +28 -0
- package/rules/tdd.md +7 -0
- package/rules/testing.md +12 -0
- package/skills/agent-prompt/SKILL.md +102 -0
- package/skills/anthropic-plan/SKILL.md +107 -0
- package/skills/everything-search/SKILL.md +144 -0
- package/skills/ingest/SKILL.md +40 -0
- package/skills/npm-creator/SKILL.md +183 -0
- package/skills/pr-review-responder/EXAMPLES.md +590 -0
- package/skills/pr-review-responder/PRINCIPLES.md +539 -0
- package/skills/pr-review-responder/README.md +209 -0
- package/skills/pr-review-responder/SKILL.md +202 -0
- package/skills/pr-review-responder/TESTING.md +407 -0
- package/skills/pr-review-responder/scripts/respond_to_reviews.py +376 -0
- package/skills/pr-review-responder/update_skill.py +297 -0
- package/skills/prompt-generator/REFERENCE.md +150 -0
- package/skills/prompt-generator/SKILL.md +154 -0
- package/skills/readability-review/SKILL.md +127 -0
- package/skills/recall/SKILL.md +27 -0
- package/skills/remember/SKILL.md +63 -0
- package/skills/rule-audit/SKILL.md +307 -0
- package/skills/rule-creator/SKILL.md +150 -0
- package/skills/skill-writer/REFERENCE.md +246 -0
- package/skills/skill-writer/SKILL.md +270 -0
- package/skills/tdd-team/SKILL.md +128 -0
|
@@ -0,0 +1,376 @@
|
|
|
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()
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Update pr-review-responder skill with all changes at once."""
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
SKILL_PATH = Path(__file__).parent / "SKILL.md"
|
|
8
|
+
|
|
9
|
+
def main() -> None:
|
|
10
|
+
content = SKILL_PATH.read_text(encoding="utf-8")
|
|
11
|
+
|
|
12
|
+
# 1. Change 7-step to 8-step
|
|
13
|
+
content = content.replace("strict 7-step protocol", "strict 8-step protocol")
|
|
14
|
+
|
|
15
|
+
# 2. Update Step 4: Change from posting to drafting
|
|
16
|
+
old_step4 = '''## STEP 4: REPLY TO EACH COMMENT INLINE (MANDATORY)
|
|
17
|
+
|
|
18
|
+
**For EACH comment, you MUST post an inline reply.**
|
|
19
|
+
|
|
20
|
+
**NOT a summary comment. NOT a general "fixed everything" comment. EACH comment gets INDIVIDUAL reply.**
|
|
21
|
+
|
|
22
|
+
1. **Response format**:
|
|
23
|
+
```
|
|
24
|
+
✅ **Fixed**: [brief description of what was changed]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
2. **Post inline reply**:
|
|
28
|
+
```bash
|
|
29
|
+
gh api repos/{owner}/{repo}/pulls/comments/{comment_id}/replies \\
|
|
30
|
+
-X POST \\
|
|
31
|
+
-f body="✅ **Fixed**: [description]"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
3. **Examples**:
|
|
35
|
+
- `✅ **Fixed**: Removed wrapper function, using direct storage.upload_file() calls`
|
|
36
|
+
- `✅ **Fixed**: Extracted shared logic to utils/view_helpers.py`
|
|
37
|
+
- `✅ **Fixed**: Moved CSS values from Python to stylesheet`
|
|
38
|
+
- `✅ **Fixed**: Added type hints to all function parameters`
|
|
39
|
+
|
|
40
|
+
**CRITICAL VALIDATION:**
|
|
41
|
+
- ☐ Did you reply to EVERY comment? (not just some)
|
|
42
|
+
- ☐ Are replies inline? (not summary comment)
|
|
43
|
+
- ☐ Did you mark reply todos complete?
|
|
44
|
+
|
|
45
|
+
**If validation fails:**
|
|
46
|
+
```
|
|
47
|
+
ERROR: Missing inline replies.
|
|
48
|
+
Found {X} comments but only {Y} replies.
|
|
49
|
+
STOPPING execution.
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Why this matters:** Reviewers need to know WHICH comments were addressed. Summary comments don't cut it.'''
|
|
53
|
+
|
|
54
|
+
new_step4 = '''## STEP 4: DRAFT REPLIES FOR EACH COMMENT (MANDATORY)
|
|
55
|
+
|
|
56
|
+
**For EACH comment, you MUST draft an inline reply for the user to post.**
|
|
57
|
+
|
|
58
|
+
**DO NOT POST COMMENTS DIRECTLY. Draft them and present to user for review.**
|
|
59
|
+
|
|
60
|
+
1. **Response format**:
|
|
61
|
+
```
|
|
62
|
+
✅ **Fixed**: [brief description of what was changed]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
2. **Draft replies in a clear format for user to copy**:
|
|
66
|
+
```
|
|
67
|
+
DRAFT REPLIES (for user to post):
|
|
68
|
+
================================
|
|
69
|
+
|
|
70
|
+
Comment #1 (file.py:45 - "description of comment"):
|
|
71
|
+
Reply: ✅ **Fixed**: [description of fix]
|
|
72
|
+
|
|
73
|
+
Comment #2 (file.py:67 - "description of comment"):
|
|
74
|
+
Reply: ✅ **Fixed**: [description of fix]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
3. **Examples**:
|
|
78
|
+
- `✅ **Fixed**: Removed wrapper function, using direct storage.upload_file() calls`
|
|
79
|
+
- `✅ **Fixed**: Extracted shared logic to utils/view_helpers.py`
|
|
80
|
+
- `✅ **Fixed**: Moved CSS values from Python to stylesheet`
|
|
81
|
+
- `✅ **Fixed**: Added type hints to all function parameters`
|
|
82
|
+
|
|
83
|
+
**CRITICAL VALIDATION:**
|
|
84
|
+
- ☐ Did you draft a reply for EVERY comment? (not just some)
|
|
85
|
+
- ☐ Are drafts specific and actionable?
|
|
86
|
+
- ☐ Did you present drafts clearly for user review?
|
|
87
|
+
|
|
88
|
+
**Why this matters:** User controls what gets posted. Drafts ensure nothing is missed while giving user final say.'''
|
|
89
|
+
|
|
90
|
+
content = content.replace(old_step4, new_step4)
|
|
91
|
+
|
|
92
|
+
# 3. Add new Step 5 (pre-push-review) before Step 5 (commits)
|
|
93
|
+
old_step5_header = '''---
|
|
94
|
+
|
|
95
|
+
## STEP 5: KEEP COMMITS SEPARATE (MANDATORY)'''
|
|
96
|
+
|
|
97
|
+
new_step5_and_6 = '''---
|
|
98
|
+
|
|
99
|
+
## STEP 5: RUN PRE-PUSH REVIEW (MANDATORY)
|
|
100
|
+
|
|
101
|
+
**BEFORE committing, you MUST run the pre-push-review skill.**
|
|
102
|
+
|
|
103
|
+
**This catches style violations, anti-patterns, and repeat mistakes BEFORE they get committed.**
|
|
104
|
+
|
|
105
|
+
1. **Invoke the pre-push-review skill**:
|
|
106
|
+
```
|
|
107
|
+
Skill(pre-push-review)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
2. **CRITICAL VALIDATION**:
|
|
111
|
+
- ☐ Did you run pre-push-review on all changed files?
|
|
112
|
+
- ☐ Did all 22 checks pass?
|
|
113
|
+
- ☐ Did you fix any violations found?
|
|
114
|
+
|
|
115
|
+
3. **If violations found**:
|
|
116
|
+
- Fix the violations FIRST
|
|
117
|
+
- Re-run pre-push-review
|
|
118
|
+
- Only proceed when all checks pass
|
|
119
|
+
|
|
120
|
+
**Why this matters:** Pre-push-review catches the EXACT patterns reviewers flag in code reviews. Running it prevents repeat mistakes.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## STEP 6: KEEP COMMITS SEPARATE (MANDATORY)'''
|
|
125
|
+
|
|
126
|
+
content = content.replace(old_step5_header, new_step5_and_6)
|
|
127
|
+
|
|
128
|
+
# 4. Renumber Step 6 -> Step 7
|
|
129
|
+
content = content.replace("## STEP 6: VERIFY ALL REPLIES POSTED", "## STEP 7: VERIFY ALL DRAFTS COMPLETE")
|
|
130
|
+
|
|
131
|
+
# 5. Update Step 7 (was 6) content
|
|
132
|
+
old_step6_content = '''**Before declaring success, you MUST verify ALL replies are visible on GitHub.**
|
|
133
|
+
|
|
134
|
+
1. **Check PR comments page**:
|
|
135
|
+
```bash
|
|
136
|
+
gh pr view {pr_number} --comments
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
2. **CRITICAL VALIDATION**:
|
|
140
|
+
- ☐ Are all replies visible?
|
|
141
|
+
- ☐ Do reply counts match comment counts?
|
|
142
|
+
- ☐ No failed posts?
|
|
143
|
+
|
|
144
|
+
3. **If validation fails**:
|
|
145
|
+
```
|
|
146
|
+
ERROR: Reply verification failed.
|
|
147
|
+
Expected {X} replies, found {Y}.
|
|
148
|
+
Check GitHub PR page manually.
|
|
149
|
+
```'''
|
|
150
|
+
|
|
151
|
+
new_step7_content = '''**Before declaring success, you MUST verify ALL reply drafts are prepared.**
|
|
152
|
+
|
|
153
|
+
1. **Review draft replies**:
|
|
154
|
+
- Count of drafts matches count of comments
|
|
155
|
+
- Each draft is specific and actionable
|
|
156
|
+
- Drafts are formatted clearly for user to copy
|
|
157
|
+
|
|
158
|
+
2. **CRITICAL VALIDATION**:
|
|
159
|
+
- ☐ Draft count matches comment count?
|
|
160
|
+
- ☐ Each draft references specific fix?
|
|
161
|
+
- ☐ Drafts presented in clear format?
|
|
162
|
+
|
|
163
|
+
3. **If validation fails**:
|
|
164
|
+
```
|
|
165
|
+
ERROR: Missing draft replies.
|
|
166
|
+
Expected {X} drafts, found {Y}.
|
|
167
|
+
Complete all drafts before proceeding.
|
|
168
|
+
```'''
|
|
169
|
+
|
|
170
|
+
content = content.replace(old_step6_content, new_step7_content)
|
|
171
|
+
|
|
172
|
+
# 6. Renumber Step 7 -> Step 8
|
|
173
|
+
content = content.replace("## STEP 7: FINAL REPORT", "## STEP 8: FINAL REPORT")
|
|
174
|
+
|
|
175
|
+
# 7. Update final report
|
|
176
|
+
old_report = '''```
|
|
177
|
+
✅ PR Review Response Complete
|
|
178
|
+
|
|
179
|
+
Fetched: {X} comments (with per_page=100)
|
|
180
|
+
Fixed: {X} issues
|
|
181
|
+
Replied: {X} inline comments (100% coverage)
|
|
182
|
+
Commits: 2 (original + review fix, NOT squashed)
|
|
183
|
+
|
|
184
|
+
TodoWrite checklist: 100% complete
|
|
185
|
+
All inline replies verified on GitHub
|
|
186
|
+
|
|
187
|
+
PR #{number}: {url}
|
|
188
|
+
|
|
189
|
+
Ready to push!
|
|
190
|
+
```'''
|
|
191
|
+
|
|
192
|
+
new_report = '''```
|
|
193
|
+
✅ PR Review Response Complete
|
|
194
|
+
|
|
195
|
+
Fetched: {X} comments (with per_page=100)
|
|
196
|
+
Fixed: {X} issues
|
|
197
|
+
Pre-push review: PASSED (all 22 checks)
|
|
198
|
+
Draft replies: {X} prepared for user
|
|
199
|
+
Commits: 2 (original + review fix, NOT squashed)
|
|
200
|
+
|
|
201
|
+
TodoWrite checklist: 100% complete
|
|
202
|
+
|
|
203
|
+
DRAFT REPLIES FOR USER TO POST:
|
|
204
|
+
================================
|
|
205
|
+
[List all draft replies here]
|
|
206
|
+
|
|
207
|
+
PR #{number}: {url}
|
|
208
|
+
|
|
209
|
+
Ready to push!
|
|
210
|
+
```'''
|
|
211
|
+
|
|
212
|
+
content = content.replace(old_report, new_report)
|
|
213
|
+
|
|
214
|
+
# 8. Update enforcement mechanisms
|
|
215
|
+
old_enforcement4 = '''4. **Step 4 violation**: Missing inline replies
|
|
216
|
+
```
|
|
217
|
+
ERROR: Missing inline replies to comments.
|
|
218
|
+
Found {X} comments, only {Y} replies posted.
|
|
219
|
+
Every comment requires individual inline reply.
|
|
220
|
+
STOPPING execution.
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
5. **Step 5 violation (ERROR)**: Commits were squashed'''
|
|
224
|
+
|
|
225
|
+
new_enforcement4_5_6 = '''4. **Step 4 violation**: Missing draft replies
|
|
226
|
+
```
|
|
227
|
+
ERROR: Missing draft replies to comments.
|
|
228
|
+
Found {X} comments, only {Y} drafts prepared.
|
|
229
|
+
Every comment requires individual draft reply.
|
|
230
|
+
STOPPING execution.
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
5. **Step 5 violation**: Pre-push review not run or failed
|
|
234
|
+
```
|
|
235
|
+
ERROR: Must run pre-push-review skill before committing.
|
|
236
|
+
This catches style violations and anti-patterns.
|
|
237
|
+
STOPPING execution.
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
6. **Step 6 violation (ERROR)**: Commits were squashed'''
|
|
241
|
+
|
|
242
|
+
content = content.replace(old_enforcement4, new_enforcement4_5_6)
|
|
243
|
+
|
|
244
|
+
# 9. Update Step 6 violation -> Step 7
|
|
245
|
+
content = content.replace(
|
|
246
|
+
'6. **Step 6 violation**: Reply verification failed',
|
|
247
|
+
'7. **Step 7 violation**: Draft verification failed'
|
|
248
|
+
)
|
|
249
|
+
content = content.replace(
|
|
250
|
+
'ERROR: Cannot verify all replies posted to GitHub.\n Check PR page manually: {url}',
|
|
251
|
+
'ERROR: Cannot verify all draft replies prepared.\n Expected {X} drafts, found {Y}.\n Complete all drafts before proceeding.'
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# 10. Update quick reference
|
|
255
|
+
old_quick_ref = '''# 4. Reply inline to EACH comment
|
|
256
|
+
gh api repos/{owner}/{repo}/pulls/comments/{comment_id}/replies -X POST -f body="✅ Fixed: ..."
|
|
257
|
+
|
|
258
|
+
# 5. Create ONE review fix commit (DON'T squash with original)'''
|
|
259
|
+
|
|
260
|
+
new_quick_ref = '''# 4. Draft replies for user (DO NOT POST)
|
|
261
|
+
# Present drafts in clear format for user to copy and post
|
|
262
|
+
|
|
263
|
+
# 5. Run pre-push-review skill
|
|
264
|
+
Skill(pre-push-review)
|
|
265
|
+
|
|
266
|
+
# 6. Create ONE review fix commit (DON'T squash with original)'''
|
|
267
|
+
|
|
268
|
+
content = content.replace(old_quick_ref, new_quick_ref)
|
|
269
|
+
|
|
270
|
+
# 11. Update remaining quick reference steps
|
|
271
|
+
content = content.replace(
|
|
272
|
+
"# 6. Verify ALL replies posted\ngh pr view {pr_number} --comments",
|
|
273
|
+
"# 7. Verify ALL draft replies complete\n# Count drafts matches count of comments"
|
|
274
|
+
)
|
|
275
|
+
content = content.replace(
|
|
276
|
+
"# 7. Push (keeps commits separate for GitHub visibility)\ngit push",
|
|
277
|
+
"# 8. Push (keeps commits separate for GitHub visibility)\ngit push"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# 12. Update root cause section
|
|
281
|
+
content = content.replace(
|
|
282
|
+
"- Did NOT reply inline to each comment\n- Did NOT verify all replies posted",
|
|
283
|
+
"- Did NOT draft replies for each comment\n- Did NOT run pre-push-review"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# 13. Update "why protocol is mandatory" section
|
|
287
|
+
content = content.replace(
|
|
288
|
+
"- ✅ Clear communication (inline reply to each)",
|
|
289
|
+
"- ✅ Clear communication (draft reply for each)"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
SKILL_PATH.write_text(content, encoding="utf-8")
|
|
293
|
+
print("Skill updated successfully!")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
if __name__ == "__main__":
|
|
297
|
+
main()
|