Skip to content
This repository was archived by the owner on Aug 18, 2026. It is now read-only.

Commit 91a58c0

Browse files
authored
ci: enable Claude + Grok PR reviewers (#16)
1 parent e71ed29 commit 91a58c0

3 files changed

Lines changed: 193 additions & 0 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Claude Review
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened]
6+
7+
permissions:
8+
contents: read
9+
pull-requests: write
10+
id-token: write
11+
12+
jobs:
13+
review:
14+
name: Claude review
15+
if: vars.CLAUDE_REVIEW_ENABLED == 'true' && vars.CLAUDE_REVIEW_CONFIGURED == 'true'
16+
runs-on: [self-hosted, linux, x64, spot-tech-ci]
17+
steps:
18+
- uses: actions/checkout@v5
19+
with:
20+
fetch-depth: 0
21+
22+
- uses: anthropics/claude-code-action@v1
23+
with:
24+
github_token: ${{ secrets.GITHUB_TOKEN }}
25+
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
26+
claude_args: "--max-turns 25"
27+
prompt: |
28+
Review this pull request for Agent Office and post concise inline review comments. Approve only if it is clean. Focus on, in priority order:
29+
30+
1. SECURITY — hook endpoint auth, unsafe file reads/writes, secret leakage, webview message-trust mistakes, path traversal in asset loading.
31+
2. CORRECTNESS — terminal adoption, transcript parsing, layout/config persistence, server lifecycle, cross-window sync regressions.
32+
3. EXTENSION UX — broken webview messaging, animation/state bugs, packaging/manifest issues, docs drift from actual behavior.
33+
34+
Be specific with file:line. Do not nitpick style.

.github/workflows/grok-review.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Grok Review
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened]
6+
7+
permissions:
8+
contents: read
9+
pull-requests: write
10+
11+
concurrency:
12+
group: grok-review-${{ github.event.pull_request.number }}
13+
cancel-in-progress: true
14+
15+
jobs:
16+
review:
17+
name: Grok review (Hermes)
18+
if: vars.GROK_REVIEW_ENABLED == 'true'
19+
runs-on: [self-hosted, linux, x64, spot-tech-ci]
20+
steps:
21+
- uses: actions/checkout@v5
22+
with:
23+
fetch-depth: 0
24+
25+
- name: Resolve Hermes proxy
26+
id: proxy
27+
run: |
28+
GW="$(ip route 2>/dev/null | awk '/default/{print $3; exit}')"
29+
for H in host.containers.internal "$GW" 172.18.0.1 172.17.0.1 127.0.0.1; do
30+
[ -z "$H" ] && continue
31+
if curl -fsS --max-time 6 "http://$H:38765/status" >/dev/null 2>&1; then
32+
echo "url=http://$H:38765/api/v1/messages" >> "$GITHUB_OUTPUT"
33+
echo "Hermes reachable at $H"
34+
exit 0
35+
fi
36+
done
37+
echo "::error::Hermes proxy (:38765) unreachable from the runner" >&2
38+
exit 1
39+
40+
- name: Grok review
41+
env:
42+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43+
REPO: ${{ github.repository }}
44+
PR_NUMBER: ${{ github.event.pull_request.number }}
45+
GROK_URL: ${{ steps.proxy.outputs.url }}
46+
run: python3 scripts/grok-review.py

scripts/grok-review.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#!/usr/bin/env python3
2+
"""Grok PR reviewer (Hermes proxy) for Agent Office."""
3+
import json
4+
import os
5+
import sys
6+
import urllib.error
7+
import urllib.request
8+
9+
API = "https://api.github.com"
10+
MARKER = "<!-- grok-review -->"
11+
MODEL = os.environ.get("GROK_MODEL", "grok-4.3")
12+
DIFF_LIMIT = 50_000
13+
14+
DEFAULT_PROMPT = """You are reviewing a pull request for Agent Office (VS Code extension + pixel-agent webview). Write ONE concise review comment in Markdown. Use `file:line` references. Do not nitpick formatting. If it is clean, say so plainly.
15+
16+
Check, in priority order:
17+
18+
1. SECURITY — hook endpoint auth, unsafe file reads/writes, secret leakage, webview message-trust mistakes, path traversal in asset loading.
19+
2. CORRECTNESS — terminal adoption, transcript parsing, layout/config persistence, server lifecycle, cross-window sync regressions.
20+
3. EXTENSION UX — broken webview messaging, animation/state bugs, packaging/manifest issues, docs drift from actual behavior.
21+
22+
End with a single final line: `Verdict: <one sentence>`.
23+
24+
Here is the unified diff:
25+
26+
```diff
27+
{diff}
28+
```"""
29+
30+
PROMPT = os.environ.get("GROK_REVIEW_PROMPT", DEFAULT_PROMPT)
31+
32+
33+
def req(method, url, token, body=None, accept="application/vnd.github+json"):
34+
headers = {"Authorization": f"Bearer {token}", "Accept": accept,
35+
"User-Agent": "grok-review"}
36+
data = None
37+
if body is not None:
38+
data = json.dumps(body).encode()
39+
headers["Content-Type"] = "application/json"
40+
r = urllib.request.Request(url, data=data, headers=headers, method=method)
41+
with urllib.request.urlopen(r, timeout=120) as resp:
42+
return resp.status, resp.read()
43+
44+
45+
def get_diff(repo, pr, token):
46+
_, raw = req("GET", f"{API}/repos/{repo}/pulls/{pr}", token,
47+
accept="application/vnd.github.v3.diff")
48+
return raw.decode("utf-8", "replace")
49+
50+
51+
def call_grok(url, diff):
52+
body = {
53+
"model": MODEL,
54+
"max_tokens": 1800,
55+
"stream": False,
56+
"messages": [{"role": "user", "content": PROMPT.format(diff=diff)}],
57+
}
58+
r = urllib.request.Request(
59+
url, data=json.dumps(body).encode(),
60+
headers={"Content-Type": "application/json",
61+
"anthropic-version": "2023-06-01"},
62+
method="POST")
63+
with urllib.request.urlopen(r, timeout=180) as resp:
64+
payload = json.loads(resp.read())
65+
parts = [b.get("text", "") for b in payload.get("content", [])
66+
if b.get("type") == "text"]
67+
return "".join(parts).strip()
68+
69+
70+
def upsert_comment(repo, pr, token, text):
71+
body_md = f"{MARKER}\n## 🔎 Grok review (Hermes · grok-4.3)\n\n{text}"
72+
_, raw = req("GET", f"{API}/repos/{repo}/issues/{pr}/comments?per_page=100", token)
73+
for c in json.loads(raw):
74+
if MARKER in (c.get("body") or ""):
75+
req("PATCH", f"{API}/repos/{repo}/issues/comments/{c['id']}", token,
76+
body={"body": body_md})
77+
return "updated"
78+
req("POST", f"{API}/repos/{repo}/issues/{pr}/comments", token,
79+
body={"body": body_md})
80+
return "created"
81+
82+
83+
def main():
84+
token = os.environ["GITHUB_TOKEN"]
85+
repo = os.environ["REPO"]
86+
pr = os.environ["PR_NUMBER"]
87+
url = os.environ["GROK_URL"]
88+
89+
diff = get_diff(repo, pr, token)
90+
if not diff.strip():
91+
print("empty diff; nothing to review")
92+
return
93+
truncated = len(diff) > DIFF_LIMIT
94+
if truncated:
95+
diff = diff[:DIFF_LIMIT] + "\n\n[... diff truncated for length ...]"
96+
97+
try:
98+
review = call_grok(url, diff)
99+
except urllib.error.HTTPError as e:
100+
print(f"grok proxy error {e.code}: {e.read()[:300]!r}", file=sys.stderr)
101+
sys.exit(1)
102+
if not review:
103+
print("model returned no text", file=sys.stderr)
104+
sys.exit(1)
105+
if truncated:
106+
review += "\n\n_Note: the diff was truncated; review covers the first part only._"
107+
108+
action = upsert_comment(repo, pr, token, review)
109+
print(f"comment {action}")
110+
111+
112+
if __name__ == "__main__":
113+
main()

0 commit comments

Comments
 (0)