mirror of
https://github.com/SeriousBug/dotfiles
synced 2026-08-17 06:46:16 -05:00
Compare commits
2 commits
3487eddb18
...
a0c3471053
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0c3471053 | ||
|
|
6f6874b27b |
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -3,3 +3,7 @@
|
|||
.dotter/cache.toml
|
||||
.dotter/cache/
|
||||
.dotter/user_vars.sh
|
||||
|
||||
# Mudpuppy skills (not tracked)
|
||||
claude/skills/mudpuppy-implementation-review/
|
||||
claude/skills/mudpuppy-pr-review/
|
||||
|
|
|
|||
|
|
@ -13,3 +13,27 @@ discord-send --attach ./screenshot.png 'Here is the rendered page'
|
|||
|
||||
When asked to respond to reviews in a PR, check the PR description and all comments. Check out the PR branch. Decide if any changes are required or not, make changes if needed, commit and push once done. Always push the PR branch if responding to a PR, do not push to a different branch.
|
||||
|
||||
## Writing Style
|
||||
|
||||
Following rules are for your writing. Follow these when responding to the user, when writing documentation and code comments, and when writing commit messages or PR descriptions.
|
||||
|
||||
- Use exact technical terms. Quote exact errors. Do not apply these rules to things you quote.
|
||||
- Drop unnecessary filler (just/really/basically/actually/simply)
|
||||
- Minimize but don't completely drop pleasantries (sure/certainly/of course/happy to/this is a great idea)
|
||||
- No hedging.
|
||||
- Short synonyms (extensive -> big, implement a solution for -> fix)
|
||||
- Do not narrate tool calling, just do it.
|
||||
- Do not use decorative tables and emoji unless asked.
|
||||
- Use well-known tech acronyms like DB/API/HTTP, do not invent new abbreviations yourself.
|
||||
- Do not use em-dashes. No "It's not X, it's Y". Avoid overusing intricate, underscore, highlight, showcase, journey to, step into. Do not use "You're absolutely right!".
|
||||
- Avoid sentence fragments for emphasis. Bad: "Not X. Not Y. Just Z."
|
||||
- Do use emphasis when necessary: "This is a destructive operation. All data will be lost. Should I proceed?"
|
||||
|
||||
Examples:
|
||||
- Bad: Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by a problem with the authentication middleware. I will investigate this problem now.
|
||||
- Bad: Not a bug. Not a feature. A fundamental design flaw.
|
||||
- Bad: It's worth noting that this approach has limitations.
|
||||
- Good: Sure. Bug may be in auth middleware. I'll check.
|
||||
- Good: This is a design flaw.
|
||||
- Good: This approach has limitations.
|
||||
|
||||
|
|
|
|||
70
claude/skills/respond-to-pr/SKILL.md
Normal file
70
claude/skills/respond-to-pr/SKILL.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
---
|
||||
name: respond-to-pr
|
||||
description: Read the reviews and comments on a GitHub PR and decide how to respond. Invoked as `/respond-to-pr 106`, `/respond-to-pr <pr-url>`, or `/respond-to-pr` (no arg, uses the PR for the current branch). Gathers everything via one helper script, evaluates each review point, fixes obvious bugs/security issues, and asks the user for the nuanced calls. May reply to bot comments; only replies to humans after the user confirms.
|
||||
---
|
||||
|
||||
You are responding to the reviews and comments on a GitHub pull request. Look
|
||||
at what the reviewers said, decide what actually needs to change, make the
|
||||
changes, and respond appropriately.
|
||||
|
||||
## Step 1: Gather the PR
|
||||
|
||||
The argument is the PR number, a PR URL, or nothing:
|
||||
|
||||
- **A number** (`106`) — pass it as-is.
|
||||
- **A URL** (`https://github.com/owner/repo/pull/106`) — pass the whole URL.
|
||||
- **Nothing** — pass nothing; the script resolves the PR for the current branch.
|
||||
|
||||
Run the helper **once**:
|
||||
|
||||
```sh
|
||||
python3 ~/.claude/skills/respond-to-pr/gather_pr.py <number-or-url-or-nothing>
|
||||
```
|
||||
|
||||
The script makes every `gh` call needed — PR title, description, all reviews,
|
||||
per-line review comments, and conversation comments, formatted as XML. **Work
|
||||
solely from its output. Do not make additional `gh` calls unless absolutely
|
||||
necessary** — the script already gives you everything.
|
||||
|
||||
If it prints `<no_pr_found>`, **stop and ask the user** which PR to work on. Do
|
||||
not guess.
|
||||
|
||||
## Step 2: Read the output
|
||||
|
||||
The XML marks every author:
|
||||
|
||||
- `is_me="true"` — written by the user's own gh account (the user, or you on
|
||||
their behalf earlier). This is not a reviewer asking you for something; it is
|
||||
what was already written. Do not "reply" to it.
|
||||
- `is_bot="true"` — a bot account (review bots, CI). You **may** respond to and
|
||||
act on these directly.
|
||||
- `is_human="true"` — a human other than the user. **Do not respond to these**
|
||||
until the user confirms (see Step 4).
|
||||
|
||||
Every `<review>`, `<comment>`, and `<line_comment>` has an `id="..."` — use it
|
||||
if the user asks you to reply to a specific comment.
|
||||
|
||||
## Step 3: Evaluate, don't obey
|
||||
|
||||
You do **not** have to do everything the reviewers say. Evaluate each point and
|
||||
decide whether it actually needs to be addressed:
|
||||
|
||||
- **Obvious bugs and security vulnerabilities — fix them.** No need to ask.
|
||||
- **Anything nuanced** (design trade-offs, style preferences, debatable
|
||||
refactors, anything where reasonable people disagree, or where you are
|
||||
unsure) — **ask the user to make the final call** before acting. Lay out the
|
||||
reviewer's point and your read on it, and let the user decide.
|
||||
|
||||
Check out the PR branch before making changes. When you make changes, commit
|
||||
and push to the **PR branch** (never a different branch), per the user's global
|
||||
review-response instructions.
|
||||
|
||||
## Step 4: Responding to comments
|
||||
|
||||
- **Bot comments** — you may reply directly (e.g. `gh pr comment`, or a reply
|
||||
to a review comment via the GitHub API using its `id`) and act on them.
|
||||
- **Human comments** — draft your intended response, **show it to the user, and
|
||||
ask for confirmation**. Only post a reply to a human after the user okays it.
|
||||
|
||||
When the user asks you to reply to a specific comment, use its `id` from the XML
|
||||
to target it.
|
||||
266
claude/skills/respond-to-pr/gather_pr.py
Executable file
266
claude/skills/respond-to-pr/gather_pr.py
Executable file
|
|
@ -0,0 +1,266 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Gather a GitHub PR's metadata, reviews, and comments into one XML report.
|
||||
|
||||
This is the data-gathering half of the respond-to-pr skill. The agent runs this
|
||||
script ONCE and works solely from its output. The script makes every `gh` call
|
||||
needed, so the agent should not make additional `gh` calls unless absolutely
|
||||
necessary.
|
||||
|
||||
Usage:
|
||||
gather_pr.py 106 # by PR number (uses current repo)
|
||||
gather_pr.py https://github.com/o/r/pull/106 # by URL
|
||||
gather_pr.py # no arg -> PR for the current branch
|
||||
|
||||
If no PR can be resolved (e.g. nothing given and the current branch has no PR),
|
||||
the script prints a <no_pr_found> marker and exits non-zero so the agent knows
|
||||
to stop and ask the user what to do.
|
||||
|
||||
The XML output marks, for every author:
|
||||
is_me="true" the comment was written by the authenticated gh user (you,
|
||||
via the user's gh access). Do NOT reply to these as if they
|
||||
were a reviewer; this is what the user (or you on their behalf)
|
||||
already wrote.
|
||||
is_bot="true" the author is a bot account. The agent MAY respond to bot
|
||||
comments directly.
|
||||
is_human="true" a human other than the authenticated user. The agent must
|
||||
NOT respond to these without the user's explicit confirmation.
|
||||
|
||||
Every review and comment carries an id="..." so the agent can reply to a
|
||||
specific comment if the user asks.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from xml.sax.saxutils import escape
|
||||
|
||||
|
||||
def gh(args, check=True):
|
||||
"""Run a gh command, return (returncode, stdout, stderr)."""
|
||||
proc = subprocess.run(
|
||||
["gh", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if check and proc.returncode != 0:
|
||||
raise RuntimeError(proc.stderr.strip() or f"gh {' '.join(args)} failed")
|
||||
return proc.returncode, proc.stdout, proc.stderr
|
||||
|
||||
|
||||
def gh_json(args):
|
||||
_, out, _ = gh(args)
|
||||
return json.loads(out) if out.strip() else None
|
||||
|
||||
|
||||
def gh_api_paginated(path):
|
||||
"""gh api with --paginate, returns a flat list."""
|
||||
_, out, _ = gh(["api", "--paginate", path])
|
||||
# --paginate concatenates JSON arrays as separate documents on separate
|
||||
# lines only when using --slurp; here gh merges into one array for array
|
||||
# endpoints, so a single json.loads works.
|
||||
out = out.strip()
|
||||
if not out:
|
||||
return []
|
||||
try:
|
||||
data = json.loads(out)
|
||||
except json.JSONDecodeError:
|
||||
# Multiple JSON arrays back to back; merge them.
|
||||
data = []
|
||||
decoder = json.JSONDecoder()
|
||||
idx = 0
|
||||
while idx < len(out):
|
||||
while idx < len(out) and out[idx].isspace():
|
||||
idx += 1
|
||||
if idx >= len(out):
|
||||
break
|
||||
obj, end = decoder.raw_decode(out, idx)
|
||||
if isinstance(obj, list):
|
||||
data.extend(obj)
|
||||
else:
|
||||
data.append(obj)
|
||||
idx = end
|
||||
return data
|
||||
|
||||
|
||||
def resolve_pr(arg):
|
||||
"""Resolve the PR. Returns dict with number, owner, repo, and base JSON,
|
||||
or None if no PR could be found."""
|
||||
fields = "number,title,body,url,author,state,isDraft,headRefName,baseRefName"
|
||||
view_args = ["pr", "view"]
|
||||
if arg:
|
||||
view_args.append(arg)
|
||||
view_args += ["--json", fields]
|
||||
code, out, err = gh(view_args, check=False)
|
||||
if code != 0:
|
||||
return None
|
||||
data = json.loads(out)
|
||||
# Parse owner/repo from the canonical PR url: github.com/{owner}/{repo}/pull/N
|
||||
m = re.search(r"github\.com/([^/]+)/([^/]+)/pull/(\d+)", data.get("url", ""))
|
||||
if not m:
|
||||
return None
|
||||
data["owner"], data["repo"] = m.group(1), m.group(2)
|
||||
data["number"] = int(m.group(3))
|
||||
return data
|
||||
|
||||
|
||||
def get_me():
|
||||
try:
|
||||
_, out, _ = gh(["api", "user", "--jq", ".login"])
|
||||
return out.strip()
|
||||
except RuntimeError:
|
||||
return None
|
||||
|
||||
|
||||
def user_attrs(user, me):
|
||||
"""Build is_me / is_bot / is_human attribute string from a GitHub user obj."""
|
||||
if not user:
|
||||
return 'login="unknown"'
|
||||
login = user.get("login", "unknown")
|
||||
utype = user.get("type", "")
|
||||
is_bot = utype == "Bot" or login.endswith("[bot]")
|
||||
is_me = me is not None and login == me
|
||||
parts = [f'login="{escape(login, {chr(34): """})}"']
|
||||
if is_me:
|
||||
parts.append('is_me="true"')
|
||||
elif is_bot:
|
||||
parts.append('is_bot="true"')
|
||||
else:
|
||||
parts.append('is_human="true"')
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def author_attrs_from_gh_view(author, me):
|
||||
"""gh pr view's author obj uses `login` and `is_bot`."""
|
||||
login = (author or {}).get("login", "unknown")
|
||||
is_bot = (author or {}).get("is_bot", False)
|
||||
is_me = me is not None and login == me
|
||||
parts = [f'login="{escape(login, {chr(34): """})}"']
|
||||
if is_me:
|
||||
parts.append('is_me="true"')
|
||||
elif is_bot:
|
||||
parts.append('is_bot="true"')
|
||||
else:
|
||||
parts.append('is_human="true"')
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def cdata(text):
|
||||
text = text or ""
|
||||
# ]]> cannot appear inside a CDATA section; split it if present.
|
||||
text = text.replace("]]>", "]]]]><![CDATA[>")
|
||||
return f"<![CDATA[{text}]]>"
|
||||
|
||||
|
||||
def main():
|
||||
arg = sys.argv[1] if len(sys.argv) > 1 else ""
|
||||
|
||||
pr = resolve_pr(arg)
|
||||
if pr is None:
|
||||
print("<no_pr_found>")
|
||||
if arg:
|
||||
print(f" Could not resolve a PR from argument: {arg}")
|
||||
else:
|
||||
print(" No PR found for the current branch, and no PR number/URL was given.")
|
||||
print("</no_pr_found>")
|
||||
sys.exit(1)
|
||||
|
||||
owner, repo, number = pr["owner"], pr["repo"], pr["number"]
|
||||
me = get_me()
|
||||
|
||||
# Reviews (the review summaries + state): pulls/{n}/reviews
|
||||
reviews = gh_api_paginated(f"repos/{owner}/{repo}/pulls/{number}/reviews")
|
||||
# Per-line review comments: pulls/{n}/comments
|
||||
line_comments = gh_api_paginated(f"repos/{owner}/{repo}/pulls/{number}/comments")
|
||||
# General conversation comments: issues/{n}/comments
|
||||
issue_comments = gh_api_paginated(f"repos/{owner}/{repo}/issues/{number}/comments")
|
||||
|
||||
# Group line comments by the review they belong to.
|
||||
by_review = {}
|
||||
orphan_line_comments = []
|
||||
for c in line_comments:
|
||||
rid = c.get("pull_request_review_id")
|
||||
if rid:
|
||||
by_review.setdefault(rid, []).append(c)
|
||||
else:
|
||||
orphan_line_comments.append(c)
|
||||
|
||||
out = []
|
||||
out.append(
|
||||
f'<pull_request number="{number}" '
|
||||
f'repo="{escape(owner)}/{escape(repo)}" '
|
||||
f'state="{escape(pr.get("state", ""))}" '
|
||||
f'draft="{str(pr.get("isDraft", False)).lower()}" '
|
||||
f'url="{escape(pr.get("url", ""))}">'
|
||||
)
|
||||
out.append(f" <title>{cdata(pr.get('title'))}</title>")
|
||||
out.append(f" <author {author_attrs_from_gh_view(pr.get('author'), me)} />")
|
||||
out.append(f" <description>{cdata(pr.get('body'))}</description>")
|
||||
|
||||
out.append(" <reviews>")
|
||||
if not reviews:
|
||||
out.append(" <!-- no formal reviews -->")
|
||||
for r in reviews:
|
||||
rid = r.get("id")
|
||||
state = r.get("state", "")
|
||||
submitted = r.get("submitted_at", "") or ""
|
||||
body = r.get("body") or ""
|
||||
kids = by_review.get(rid, [])
|
||||
# Skip empty pending/commented reviews that carry no body and no comments.
|
||||
if not body.strip() and not kids and state in ("COMMENTED", "PENDING"):
|
||||
continue
|
||||
out.append(
|
||||
f' <review id="{rid}" {user_attrs(r.get("user"), me)} '
|
||||
f'state="{escape(state)}" submitted_at="{escape(submitted)}">'
|
||||
)
|
||||
if body.strip():
|
||||
out.append(f" <body>{cdata(body)}</body>")
|
||||
for c in kids:
|
||||
out.append(_render_line_comment(c, me, indent=" "))
|
||||
out.append(" </review>")
|
||||
out.append(" </reviews>")
|
||||
|
||||
if orphan_line_comments:
|
||||
out.append(" <line_comments_outside_reviews>")
|
||||
for c in orphan_line_comments:
|
||||
out.append(_render_line_comment(c, me, indent=" "))
|
||||
out.append(" </line_comments_outside_reviews>")
|
||||
|
||||
out.append(" <conversation_comments>")
|
||||
if not issue_comments:
|
||||
out.append(" <!-- no conversation comments -->")
|
||||
for c in issue_comments:
|
||||
out.append(
|
||||
f' <comment id="{c.get("id")}" {user_attrs(c.get("user"), me)} '
|
||||
f'created_at="{escape(c.get("created_at", "") or "")}">'
|
||||
)
|
||||
out.append(f" <body>{cdata(c.get('body'))}</body>")
|
||||
out.append(" </comment>")
|
||||
out.append(" </conversation_comments>")
|
||||
|
||||
out.append("</pull_request>")
|
||||
print("\n".join(out))
|
||||
|
||||
|
||||
def _render_line_comment(c, me, indent):
|
||||
attrs = [
|
||||
f'id="{c.get("id")}"',
|
||||
user_attrs(c.get("user"), me),
|
||||
f'path="{escape(c.get("path", "") or "")}"',
|
||||
]
|
||||
line = c.get("line") or c.get("original_line")
|
||||
if line is not None:
|
||||
attrs.append(f'line="{line}"')
|
||||
if c.get("in_reply_to_id"):
|
||||
attrs.append(f'in_reply_to="{c["in_reply_to_id"]}"')
|
||||
parts = [f"{indent}<line_comment {' '.join(attrs)}>"]
|
||||
diff = (c.get("diff_hunk") or "").strip()
|
||||
if diff:
|
||||
parts.append(f"{indent} <diff_hunk>{cdata(diff)}</diff_hunk>")
|
||||
parts.append(f"{indent} <body>{cdata(c.get('body'))}</body>")
|
||||
parts.append(f"{indent}</line_comment>")
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in a new issue