Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 134 additions & 18 deletions multi-review/run-multi-review.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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<details>\n<summary>{status_label} {r['name']}</summary>\n\n{output}\n</details>\n"
)
Expand All @@ -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)

Expand All @@ -441,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],
capture_output=True, text=True, env=os.environ.copy(), timeout=30,
[gh_path, "api", "-X", "POST",
f"/repos/{repository}/issues/{pr_number}/comments",
"--input", "-"],
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:
Expand Down Expand Up @@ -521,6 +564,69 @@ def cleanup_error_comments() -> None:
pass


def cleanup_reviewer_comments(keep_comment_id: int | None = None) -> None:
"""Delete per-reviewer comments, keeping the coordinator comment.

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:
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

# 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:
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()
Expand Down Expand Up @@ -666,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
Expand All @@ -683,11 +793,17 @@ 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(keep_comment_id=posted_id)
except Exception as e:
print(f"Failed to cleanup reviewer comments: {e}", file=sys.stderr)

return 0


Expand Down
Loading