Skip to content

Commit d5cdd03

Browse files
DavidRajnohaclaude
andcommitted
wip: add GitHub PR comment review flow for CI iteration
- review-github.py: two-way review via PR comments with code-enforced author filtering (.user.login match) and time-scoping - iterate-ci-flaky: integrate GitHub review + Slack webhook notifications - ideas doc: mark Slack/GitHub review as implemented Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 34f5205 commit d5cdd03

3 files changed

Lines changed: 267 additions & 16 deletions

File tree

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
#!/usr/bin/env python3
2+
"""GitHub PR comment-based review flow for agentic test iteration.
3+
4+
Posts fix details as PR comments and polls for author replies within a
5+
timed review window. Designed to work alongside Slack webhook notifications
6+
(one-way) — GitHub PR comments provide the two-way interaction channel.
7+
8+
Usage:
9+
# Post a review comment on a PR
10+
python3 review-github.py post <pr_number> <message> [--repo owner/repo]
11+
12+
# Wait for author reply within a review window
13+
python3 review-github.py wait <pr_number> <since_timestamp> [--timeout 600] [--repo owner/repo]
14+
15+
Output formats:
16+
post: COMMENT_ID=<id> COMMENT_TIME=<iso_timestamp>
17+
wait: REPLY=<text> (author replied)
18+
NO_REPLY (timeout reached, no author reply)
19+
20+
Requires: gh CLI authenticated with comment access to the target repo.
21+
22+
Security: Author filtering is enforced deterministically in code —
23+
the PR author's login is fetched via API and only comments from that
24+
user are considered. This is not instruction-based filtering.
25+
"""
26+
27+
import argparse
28+
import json
29+
import subprocess
30+
import sys
31+
import time
32+
from datetime import datetime, timezone
33+
34+
35+
DEFAULT_REPO = "openshift/monitoring-plugin"
36+
MAGIC_PREFIX = "/agent"
37+
38+
39+
def gh_api(endpoint, method="GET", body=None, repo=None):
40+
"""Call GitHub API via gh CLI."""
41+
cmd = ["gh", "api"]
42+
if repo:
43+
endpoint = endpoint.replace("{repo}", repo)
44+
if method != "GET":
45+
cmd.extend(["--method", method])
46+
if body:
47+
for key, value in body.items():
48+
cmd.extend(["-f", f"{key}={value}"])
49+
cmd.append(endpoint)
50+
51+
result = subprocess.run(cmd, capture_output=True, text=True)
52+
if result.returncode != 0:
53+
print(f"gh api failed: {result.stderr.strip()}", file=sys.stderr)
54+
return None
55+
56+
if not result.stdout.strip():
57+
return {}
58+
59+
try:
60+
return json.loads(result.stdout)
61+
except json.JSONDecodeError:
62+
print(f"Invalid JSON from gh api: {result.stdout[:200]}", file=sys.stderr)
63+
return None
64+
65+
66+
def get_pr_author(pr, repo):
67+
"""Fetch the PR author's login."""
68+
data = gh_api(f"repos/{repo}/pulls/{pr}")
69+
if data and "user" in data:
70+
return data["user"]["login"]
71+
return None
72+
73+
74+
def post_comment(pr, message, repo):
75+
"""Post a comment on a PR. Returns (comment_id, created_at)."""
76+
data = gh_api(
77+
f"repos/{repo}/issues/{pr}/comments",
78+
method="POST",
79+
body={"body": message},
80+
)
81+
if data and "id" in data:
82+
comment_id = data["id"]
83+
created_at = data.get("created_at", "")
84+
print(f"COMMENT_ID={comment_id}")
85+
print(f"COMMENT_TIME={created_at}")
86+
return comment_id, created_at
87+
88+
print("Failed to post comment", file=sys.stderr)
89+
return None, None
90+
91+
92+
def wait_for_author_reply(pr, since_timestamp, repo, timeout=600, poll_interval=30):
93+
"""Poll PR comments for a reply from the PR author.
94+
95+
Only considers comments that:
96+
1. Were posted AFTER since_timestamp (time-scoped)
97+
2. Were authored by the PR author (deterministic .user.login check)
98+
3. Optionally start with the magic prefix /agent (if present, stripped from reply)
99+
100+
Args:
101+
pr: PR number
102+
since_timestamp: ISO 8601 timestamp — only comments after this are considered
103+
repo: owner/repo string
104+
timeout: seconds to wait before giving up
105+
poll_interval: seconds between polls
106+
107+
Returns:
108+
Reply text if found, None otherwise.
109+
"""
110+
# Fetch PR author login — deterministic, code-enforced filter
111+
pr_author = get_pr_author(pr, repo)
112+
if not pr_author:
113+
print("Could not determine PR author. Proceeding without review.", file=sys.stderr)
114+
print("NO_REPLY")
115+
return None
116+
117+
print(f"Waiting up to {timeout}s for reply from @{pr_author} on PR #{pr}...", flush=True)
118+
119+
deadline = time.time() + timeout
120+
seen_ids = set()
121+
122+
while time.time() < deadline:
123+
# Fetch comments created after since_timestamp
124+
comments = gh_api(
125+
f"repos/{repo}/issues/{pr}/comments?since={since_timestamp}&per_page=50"
126+
)
127+
128+
if comments is None:
129+
remaining = int(deadline - time.time())
130+
if remaining > 0:
131+
print(f"API error, retrying in {poll_interval}s ({remaining}s remaining)...",
132+
file=sys.stderr, flush=True)
133+
time.sleep(min(poll_interval, max(1, remaining)))
134+
continue
135+
136+
for comment in comments:
137+
comment_id = comment.get("id")
138+
if comment_id in seen_ids:
139+
continue
140+
seen_ids.add(comment_id)
141+
142+
# Deterministic author filter — code-enforced, not instruction-based
143+
commenter = comment.get("user", {}).get("login", "")
144+
if commenter != pr_author:
145+
continue
146+
147+
body = comment.get("body", "").strip()
148+
149+
# If magic prefix is used, strip it; otherwise accept any author comment
150+
if body.startswith(MAGIC_PREFIX):
151+
body = body[len(MAGIC_PREFIX):].strip()
152+
153+
if body:
154+
print(f"REPLY={body}")
155+
return body
156+
157+
remaining = int(deadline - time.time())
158+
if remaining > 0:
159+
print(
160+
f"No reply yet from @{pr_author}, {remaining}s remaining...",
161+
file=sys.stderr,
162+
flush=True,
163+
)
164+
time.sleep(min(poll_interval, max(1, remaining)))
165+
166+
print("NO_REPLY")
167+
return None
168+
169+
170+
def format_fix_comment(message):
171+
"""Wrap the agent's message in a standard comment format."""
172+
return (
173+
"### Agent: Fix Applied\n\n"
174+
f"{message}\n\n"
175+
"---\n"
176+
f"*Reply to this comment (or prefix with `{MAGIC_PREFIX}`) to provide feedback. "
177+
"The agent will incorporate your input before pushing, or proceed automatically "
178+
"after the review window expires.*"
179+
)
180+
181+
182+
def cmd_post(args):
183+
"""Handle the 'post' subcommand."""
184+
formatted = format_fix_comment(args.message)
185+
comment_id, created_at = post_comment(args.pr, formatted, args.repo)
186+
return 0 if comment_id else 1
187+
188+
189+
def cmd_wait(args):
190+
"""Handle the 'wait' subcommand."""
191+
wait_for_author_reply(
192+
args.pr, args.since, args.repo, timeout=args.timeout
193+
)
194+
return 0
195+
196+
197+
def main():
198+
parser = argparse.ArgumentParser(
199+
description="GitHub PR comment-based review for agentic test iteration"
200+
)
201+
parser.add_argument(
202+
"--repo", default=DEFAULT_REPO,
203+
help=f"GitHub repo (default: {DEFAULT_REPO})"
204+
)
205+
subparsers = parser.add_subparsers(dest="command", required=True)
206+
207+
# 'post' subcommand
208+
post_parser = subparsers.add_parser("post", help="Post a review comment on a PR")
209+
post_parser.add_argument("pr", help="PR number")
210+
post_parser.add_argument("message", help="Comment body (markdown supported)")
211+
212+
# 'wait' subcommand
213+
wait_parser = subparsers.add_parser(
214+
"wait", help="Wait for author reply on a PR"
215+
)
216+
wait_parser.add_argument("pr", help="PR number")
217+
wait_parser.add_argument("since", help="ISO 8601 timestamp — only consider comments after this")
218+
wait_parser.add_argument(
219+
"--timeout", type=int, default=600,
220+
help="Seconds to wait for reply (default: 600)"
221+
)
222+
223+
args = parser.parse_args()
224+
225+
if args.command == "post":
226+
return cmd_post(args)
227+
elif args.command == "wait":
228+
return cmd_wait(args)
229+
230+
231+
if __name__ == "__main__":
232+
sys.exit(main())

.claude/commands/iterate-ci-flaky.md

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -78,26 +78,34 @@ Required in `.claude/settings.local.json`:
7878
}
7979
```
8080

81-
### 3. Slack Notifications (optional)
81+
### 3. Notifications & Review (optional)
8282

83-
Notifications are optional — if not configured, the script prints to stdout and the loop continues normally.
83+
Notifications and review are optional — if not configured, the script prints to stdout and the loop continues normally.
8484

85-
**Option A (one-way — webhook):**
85+
**Slack Notifications (one-way):**
8686
```bash
8787
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/T.../B.../..."
8888
```
89-
Setup: Slack → Apps → Incoming Webhooks → create webhook for your channel.
89+
Setup: Slack → Apps → Incoming Webhooks → create webhook for your channel. 5 minutes.
90+
Provides one-way status notifications at key events (ci_started, ci_failed, fix_applied, etc.).
9091

91-
**Option B (two-way — bot with thread replies):**
92-
```bash
93-
export SLACK_BOT_TOKEN="xoxb-..."
94-
export SLACK_CHANNEL_ID="C0123456789"
95-
```
96-
Setup: Create a Slack App at api.slack.com/apps with scopes `chat:write`, `channels:history`. Install to workspace. Invite the bot to the target channel.
92+
**GitHub PR Comment Review (two-way):**
93+
94+
The `review-window` parameter enables a two-way review flow using GitHub PR comments. When a fix is ready:
95+
96+
1. Agent posts fix details as a PR comment (via `review-github.py post`)
97+
2. Agent also sends a Slack webhook notification (if configured)
98+
3. Agent waits `review-window` seconds for a reply from the **PR author only**
99+
4. If the author replies on the PR — agent reads the feedback and adjusts the fix
100+
5. If no reply within the window — agent proceeds autonomously
101+
102+
**Security**: Author filtering is **code-enforced** in `review-github.py` — only comments where `.user.login` matches the PR author are considered. This is deterministic, not instruction-based.
103+
104+
**How to reply**: Post a regular comment on the PR. The agent only reads comments from the PR author posted after the agent's notification. Optionally prefix with `/agent` for clarity.
97105

98-
Option B enables the `review-window` parameter — after posting a fix, the agent waits for your reply in the Slack thread before pushing.
106+
No additional setup needed beyond `gh auth` (Step 1) — the same token used for `/test` comments is used for posting and reading review comments.
99107

100-
Both can be set in `cypress/export-env.sh` or `~/.zshrc`.
108+
Both Slack webhook URL and review-window can be set in `cypress/export-env.sh` or `~/.zshrc`.
101109

102110
### 4. Unsigned Commits
103111

@@ -255,22 +263,31 @@ For each fixable failure:
255263

256264
5. **Notify and review window** (before pushing):
257265

258-
Post the fix details to Slack:
266+
**a) Slack notification** (one-way, if configured):
259267
```bash
260268
python3 .claude/commands/cypress/scripts/notify-slack.py send fix_applied "*What changed:*\n• {file}: {change_description}\n\n*Why:* {diagnosis_summary}\n*Classification:* {classification} (confidence: {confidence})\n\n`git diff HEAD~1` on branch `{headRefName}`" --pr {pr} --branch {headRefName}
261269
```
262270

263-
If `review-window` > 0 and Option B is configured, wait for user feedback:
271+
**b) GitHub PR review comment** (two-way, if `review-window` > 0):
272+
273+
Post fix details as a PR comment:
264274
```bash
265-
python3 .claude/commands/cypress/scripts/notify-slack.py wait {MESSAGE_TS} --timeout {review-window}
275+
python3 .claude/commands/cypress/scripts/review-github.py post {pr} "**What changed:**\n• {file}: {change_description}\n\n**Why:** {diagnosis_summary}\n**Classification:** {classification} (confidence: {confidence})\n\n\`git diff HEAD~1\` on branch \`{headRefName}\`"
276+
```
277+
278+
Capture `COMMENT_TIME` from the output, then wait for author reply:
279+
```bash
280+
python3 .claude/commands/cypress/scripts/review-github.py wait {pr} {COMMENT_TIME} --timeout {review-window}
266281
```
267282

268283
Parse the output:
269-
- `REPLY=<text>`: User provided feedback. Read the reply text and adjust the fix accordingly. This may mean:
284+
- `REPLY=<text>`: PR author provided feedback. Read the reply text and adjust the fix accordingly. This may mean:
270285
- Reverting the commit (`git reset --soft HEAD~1`), applying the user's suggestion, and re-committing
271286
- Or making an additional commit on top with the adjustment
272287
- `NO_REPLY`: No feedback within the window. Proceed with push.
273288

289+
**Note**: The `wait` command only considers comments from the PR author (`.user.login` match, code-enforced). Comments from other users or bots are ignored.
290+
274291
6. **Push**:
275292
```bash
276293
git push origin {headRefName}

docs/agentic-test-iteration-ideas.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ The orchestrator could automatically transition from Phase A to Phase B when loc
128128

129129
## Slack Notifications for Long-Running Loops
130130

131+
**Status**: Implemented. Slack webhook notifications (Option A) integrated into `/iterate-ci-flaky`. GitHub PR comment-based review flow implemented as the two-way interaction channel (`review-github.py`). Option B (Slack bot with thread replies) documented but deprioritized due to internal setup complexity.
132+
131133
### The Problem
132134

133135
The CI iteration loop (`/iterate-ci-flaky`) runs for hours — each CI run takes ~2h, and the loop may do 3-5 fix-push-wait cycles. During that time:

0 commit comments

Comments
 (0)