From e776c21971e691e67b07a19b1634c2c683ccfc3f Mon Sep 17 00:00:00 2001 From: Pinghua Date: Wed, 10 Jun 2026 15:56:43 +0800 Subject: [PATCH] change stdout --- scripts/review_pr.py | 109 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 86 insertions(+), 23 deletions(-) diff --git a/scripts/review_pr.py b/scripts/review_pr.py index fca864a..23a592f 100644 --- a/scripts/review_pr.py +++ b/scripts/review_pr.py @@ -317,9 +317,11 @@ def build_prompt(meta, files, diff_path, agent_md, impact_report_path=None): def run_claude(claude_bin, project_root, prompt, extra_dir=None): - """Run claude -p, return stdout.""" + """Run claude with stream-json output, return all text lines.""" cmd = [ claude_bin, "-p", + "--output-format", "stream-json", + "--include-partial-messages", "--permission-mode", "acceptEdits", "--verbose", ] @@ -327,7 +329,7 @@ def run_claude(claude_bin, project_root, prompt, extra_dir=None): cmd += ["--add-dir", extra_dir] cmd.append(prompt) - print(f"[review_pr] Running: {' '.join(cmd[:4])} ...", file=sys.stderr) + print(f"[review_pr] Running claude -p (stream-json) ...", file=sys.stderr) proc = subprocess.Popen( cmd, cwd=project_root, @@ -340,9 +342,33 @@ def run_claude(claude_bin, project_root, prompt, extra_dir=None): def _stream(pipe, label, dest_lines): for line in pipe: line = line.rstrip("\n") - if line: - print(f"[{label}] {line}", file=sys.stderr) + if not line: + continue dest_lines.append(line) + # Pretty-print stream-json events for progress visibility + try: + msg = json.loads(line) + t = msg.get("type", "") + if t == "assistant": + text = "" + for block in msg.get("message", {}).get("content", []): + if block.get("type") == "text": + text = block.get("text", "") + if text: + print(f"[claude] {text}", file=sys.stderr) + elif t == "tool_use": + name = msg.get("message", {}).get("name", "?") + inp = msg.get("message", {}).get("input", {}) + print(f"[claude] tool: {name}({json.dumps(inp, ensure_ascii=False)})", file=sys.stderr) + elif t == "tool_result": + print(f"[claude] tool: result", file=sys.stderr) + elif t == "system": + subtype = msg.get("subtype", "") + if subtype == "init": + print(f"[claude] system: init", file=sys.stderr) + # skip heartbeat/user/other events to avoid noise + except (json.JSONDecodeError, KeyError, TypeError): + print(f"[claude] {line}", file=sys.stderr) stderr_lines = [] t = threading.Thread(target=_stream, args=(proc.stderr, "claude:stderr", stderr_lines)) @@ -363,30 +389,67 @@ def run_claude(claude_bin, project_root, prompt, extra_dir=None): def extract_json_from_output(stdout): - """Extract the review JSON from Claude's output.""" - match = re.search(r"```json\s*([\s\S]*)\s*```", stdout) - if not match: - print("ERROR: No ```json block found in Claude output.", file=sys.stderr) - print(stdout[:3000], file=sys.stderr) - sys.exit(1) + """Extract the review JSON from Claude's output. + Handles both stream-json lines and plain-text ```json blocks. + """ + # ── Try stream-json: collect assistant text ── + assistant_text = _extract_stream_json_text(stdout) + if assistant_text: + obj = _find_review_json(assistant_text) + if obj: + return obj + print("[review_pr] stream-json text gathered but no review JSON found; trying raw ...", + file=sys.stderr) + + # ── Fall back to plain-text ```json block ── + obj = _find_review_json(stdout) + if obj: + return obj + + print("ERROR: No valid review JSON found in Claude output.", file=sys.stderr) + print(stdout[-3000:], file=sys.stderr) + sys.exit(1) + + +def _extract_stream_json_text(stdout): + """Parse stdout as stream-json lines and return concatenated assistant text.""" + texts = [] + for line in stdout.splitlines(): + line = line.strip() + if not line or not line.startswith("{"): + continue + try: + msg = json.loads(line) + except json.JSONDecodeError: + continue + if msg.get("type") != "assistant": + continue + for block in msg.get("message", {}).get("content", []): + if block.get("type") == "text": + texts.append(block.get("text", "")) + return "\n".join(texts) if texts else "" + + +def _find_review_json(text): + """Find and parse a review JSON object in *text*. + + Returns the parsed dict, or None. + """ + match = re.search(r"```json\s*([\s\S]*)\s*```", text) + if not match: + return None try: obj = json.loads(match.group(1)) - except json.JSONDecodeError as e: - print(f"ERROR: Invalid JSON in Claude output: {e}", file=sys.stderr) - print(match.group(1)[:2000], file=sys.stderr) - sys.exit(1) - + except json.JSONDecodeError: + return None + if not isinstance(obj, dict): + return None # Unwrap {review: {...}} envelope if present - if isinstance(obj, dict) and "review" in obj and isinstance(obj["review"], dict): + if "review" in obj and isinstance(obj["review"], dict): obj = obj["review"] - - if not isinstance(obj, dict) or "body" not in obj: - print(f"ERROR: JSON missing required 'body' field. " - f"Keys: {list(obj.keys()) if isinstance(obj, dict) else type(obj).__name__}", - file=sys.stderr) - sys.exit(1) - + if "body" not in obj: + return None return obj