From 07544d00669a9cf11e14cbc5183055cd428b4867 Mon Sep 17 00:00:00 2001 From: svtter Date: Thu, 21 May 2026 16:55:48 +0800 Subject: [PATCH 1/3] feat(multi-review): output single coordinator comment with noise filtering - Add _filter_noise() to strip CLI boilerplate (tool calls, log lines, session metadata) from reviewer output - Add cleanup_reviewer_comments() to delete per-reviewer comments after coordinator synthesis is posted, keeping only the final comment - post_pr_comment() now returns comment ID for precise cleanup targeting - Keep USE_GITHUB_TOKEN=true to avoid OIDC crash in opencode CLI Co-Authored-By: Claude Opus 4.7 --- multi-review/run-multi-review.py | 110 ++++++++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 3 deletions(-) diff --git a/multi-review/run-multi-review.py b/multi-review/run-multi-review.py index 752a32d..d083cdb 100644 --- a/multi-review/run-multi-review.py +++ b/multi-review/run-multi-review.py @@ -403,6 +403,44 @@ def _strip_ansi(raw: str) -> str: return text.strip() +_NOISE_PREFIXES = ( + "asserting permissions", + "adding reaction", + "removing reaction", + "fetching prompt data", + "checking out local branch", + "sending message to opencode", + "checking if branch is dirty", + "creating comment", + "pushing to local branch", + "opencode session ses_", + "performing one time database", + "sqlite-migration", + "database migration", +) +_TOOL_LINE_RE = re.compile(r"^\|\s+(Shell|Read|Write|Edit|Bash)\s") +_LOG_LINE_RE = re.compile(r"^\[\d{2}:\d{2}:\d{2}\.\d{3}\]\s+(INFO|WARN|ERROR|DEBUG)") + + +def _filter_noise(text: str) -> str: + """Remove opencode CLI boilerplate lines from reviewer output.""" + cleaned = [] + for line in text.splitlines(): + stripped = line.strip() + if stripped.startswith(_NOISE_PREFIXES): + continue + if _TOOL_LINE_RE.match(stripped): + continue + if _LOG_LINE_RE.match(stripped): + continue + if "opencode.ai/s/" in stripped: + continue + if stripped.startswith("| ") and '{"' in stripped: + continue + cleaned.append(line) + return "\n".join(cleaned).strip() + + def _truncate(text: str, limit: int = 8000) -> str: text = text.strip() if len(text) > limit: @@ -412,10 +450,10 @@ def _truncate(text: str, limit: int = 8000) -> str: def format_pr_comment(coordinator_output: str, reviewer_results: list[dict[str, Any]]) -> str: """Format the final PR comment with coordinator output and collapsible reviewer details.""" - parts = [_strip_ansi(coordinator_output).strip(), "\n\n---\n**详细审查报告:**\n"] + parts = [_filter_noise(_strip_ansi(coordinator_output)).strip(), "\n\n---\n**详细审查报告:**\n"] for r in reviewer_results: status_label = "✅" if r["status"] == "success" else "⚠️" - output = _truncate(_strip_ansi(r.get("output", ""))) + output = _truncate(_filter_noise(_strip_ansi(r.get("output", "")))) parts.append( f"\n
\n{status_label} {r['name']}\n\n{output}\n
\n" ) @@ -426,7 +464,7 @@ def post_fallback_comment(reviewer_results: list[dict[str, Any]]) -> str: """Format a fallback comment with raw reviewer outputs when coordinator fails.""" parts = ["⚠️ Coordinator agent failed. Showing raw reviewer outputs:\n"] for r in reviewer_results: - output = _truncate(_strip_ansi(r.get("output", ""))) + output = _truncate(_filter_noise(_strip_ansi(r.get("output", "")))) parts.append(f"\n### {r['name']} ({r['status']})\n\n{output}\n") return "".join(parts) @@ -521,6 +559,66 @@ def cleanup_error_comments() -> None: pass +def cleanup_reviewer_comments() -> None: + """Delete per-reviewer comments, keeping only the coordinator comment. + + Called after the coordinator comment is posted. Identifies comments from + the same CI run and deletes all except the most recent one (the coordinator + synthesis). + """ + ctx = _get_pr_context() + if not ctx: + return + pr_number, repository = ctx + + github_run_id = get_env("GITHUB_RUN_ID", "") + if not github_run_id: + return + + gh_path = shutil.which("gh") + if not gh_path: + return + + run_link_pattern = f"/{repository}/actions/runs/{github_run_id}" + + try: + result = subprocess.run( + [gh_path, "api", "-H", "Accept: application/vnd.github+json", + f"/repos/{repository}/issues/{pr_number}/comments"], + capture_output=True, text=True, env=os.environ.copy(), timeout=30, + ) + if result.returncode != 0: + return + comments = json.loads(result.stdout) + except Exception: + return + + # Find comments from this CI run + run_comments = [ + c for c in comments + if run_link_pattern in c.get("body", "") + ] + + if len(run_comments) <= 1: + return + + # The last comment is the coordinator synthesis — keep it, delete the rest + to_delete = run_comments[:-1] + for comment in to_delete: + comment_id = comment.get("id") + if not comment_id: + continue + try: + subprocess.run( + [gh_path, "api", "-X", "DELETE", + f"/repos/{repository}/issues/comments/{comment_id}"], + capture_output=True, text=True, env=os.environ.copy(), timeout=10, + ) + except Exception: + pass + + print(f"Cleaned up {len(to_delete)} per-reviewer comment(s), kept coordinator comment", file=sys.stderr) + def main() -> int: try: return _main() @@ -688,6 +786,12 @@ def _main() -> int: print("Could not post to PR via gh CLI, writing to stdout as fallback", file=sys.stderr) print(comment) + # Clean up per-reviewer comments, keep only the coordinator synthesis + try: + cleanup_reviewer_comments() + except Exception as e: + print(f"Failed to cleanup reviewer comments: {e}", file=sys.stderr) + return 0 From 257cbaf22f93062ebf78573cb2dda1bffd42b351 Mon Sep 17 00:00:00 2001 From: svtter Date: Thu, 21 May 2026 17:02:44 +0800 Subject: [PATCH 2/3] fix(multi-review): use gh api for comment posting with ID return MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use `gh api --input` instead of `gh pr comment` so we get the comment ID in the response. Pass this ID to cleanup_reviewer_comments() for precise targeting — avoids relying on comment ordering which is not guaranteed with concurrent reviewers. Co-Authored-By: Claude Opus 4.7 --- multi-review/run-multi-review.py | 56 +++++++++++++++++++------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/multi-review/run-multi-review.py b/multi-review/run-multi-review.py index d083cdb..dbd1787 100644 --- a/multi-review/run-multi-review.py +++ b/multi-review/run-multi-review.py @@ -479,30 +479,35 @@ def _get_pr_context() -> tuple[str, str] | None: return match.group(1), github_repository -def post_pr_comment(body: str) -> bool: - """Post a comment to the current PR using gh CLI. Returns True on success.""" +def post_pr_comment(body: str) -> int | None: + """Post a comment to the current PR via gh api. Returns comment ID on success.""" ctx = _get_pr_context() if not ctx: - return False + return None pr_number, repository = ctx gh_path = shutil.which("gh") if not gh_path: - return False + return None try: + payload = json.dumps({"body": body}) result = subprocess.run( - [gh_path, "pr", "comment", pr_number, "--repo", repository, "--body", body], + [gh_path, "api", "-X", "POST", + f"/repos/{repository}/issues/{pr_number}/comments", + "--input", payload], capture_output=True, text=True, env=os.environ.copy(), timeout=30, ) - if result.returncode == 0: - print(f"Posted synthesized review comment to PR #{pr_number}", file=sys.stderr) - return True - print(f"Failed to post PR comment: {result.stderr}", file=sys.stderr) - return False + if result.returncode != 0: + print(f"Failed to post PR comment: {result.stderr}", file=sys.stderr) + return None + data = json.loads(result.stdout) + comment_id = data.get("id") + print(f"Posted synthesized review comment to PR #{pr_number} (id={comment_id})", file=sys.stderr) + return comment_id except Exception as e: print(f"Failed to post PR comment: {e}", file=sys.stderr) - return False + return None def cleanup_error_comments() -> None: @@ -559,12 +564,12 @@ def cleanup_error_comments() -> None: pass -def cleanup_reviewer_comments() -> None: - """Delete per-reviewer comments, keeping only the coordinator comment. +def cleanup_reviewer_comments(keep_comment_id: int | None = None) -> None: + """Delete per-reviewer comments, keeping the coordinator comment. - Called after the coordinator comment is posted. Identifies comments from - the same CI run and deletes all except the most recent one (the coordinator - synthesis). + Uses keep_comment_id (returned by post_pr_comment) to identify the + coordinator comment. Falls back to keeping the latest run comment if + keep_comment_id is unavailable. """ ctx = _get_pr_context() if not ctx: @@ -602,8 +607,11 @@ def cleanup_reviewer_comments() -> None: if len(run_comments) <= 1: return - # The last comment is the coordinator synthesis — keep it, delete the rest - to_delete = run_comments[:-1] + # Keep the coordinator comment (identified by keep_comment_id or latest) + if keep_comment_id: + to_delete = [c for c in run_comments if c.get("id") != keep_comment_id] + else: + to_delete = run_comments[:-1] for comment in to_delete: comment_id = comment.get("id") if not comment_id: @@ -764,7 +772,11 @@ def _main() -> int: if remaining_time <= 0: print("No time left for coordinator, posting raw outputs", file=sys.stderr) comment = post_fallback_comment(reviewer_results) - post_pr_comment(comment) + posted_id = post_pr_comment(comment) + try: + cleanup_reviewer_comments(keep_comment_id=posted_id) + except Exception: + pass return 0 coord_timeout = min(coordinator_timeout, remaining_time) if global_deadline else coordinator_timeout @@ -781,14 +793,14 @@ def _main() -> int: comment = post_fallback_comment(reviewer_results) # Post synthesized comment to PR - posted = post_pr_comment(comment) - if not posted: + posted_id = post_pr_comment(comment) + if not posted_id: print("Could not post to PR via gh CLI, writing to stdout as fallback", file=sys.stderr) print(comment) # Clean up per-reviewer comments, keep only the coordinator synthesis try: - cleanup_reviewer_comments() + cleanup_reviewer_comments(keep_comment_id=posted_id) except Exception as e: print(f"Failed to cleanup reviewer comments: {e}", file=sys.stderr) From eba1555114c5965b63e5396bd4fd20797413ca65 Mon Sep 17 00:00:00 2001 From: svtter Date: Thu, 21 May 2026 17:09:09 +0800 Subject: [PATCH 3/3] fix(multi-review): pipe JSON body via stdin for gh api --input gh api --input expects a file path, not inline data. Use stdin pipe (--input -) with subprocess.run(input=...) instead. Co-Authored-By: Claude Opus 4.7 --- multi-review/run-multi-review.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/multi-review/run-multi-review.py b/multi-review/run-multi-review.py index dbd1787..bb619bb 100644 --- a/multi-review/run-multi-review.py +++ b/multi-review/run-multi-review.py @@ -495,8 +495,8 @@ def post_pr_comment(body: str) -> int | None: result = subprocess.run( [gh_path, "api", "-X", "POST", f"/repos/{repository}/issues/{pr_number}/comments", - "--input", payload], - capture_output=True, text=True, env=os.environ.copy(), timeout=30, + "--input", "-"], + input=payload, capture_output=True, text=True, env=os.environ.copy(), timeout=30, ) if result.returncode != 0: print(f"Failed to post PR comment: {result.stderr}", file=sys.stderr)