change stdout

This commit is contained in:
Pinghua
2026-06-10 15:56:43 +08:00
parent 971ee4dfff
commit 89709cec6e
+49 -24
View File
@@ -15,10 +15,12 @@ import argparse
import json import json
import os import os
import re import re
import shlex
import shutil import shutil
import subprocess import subprocess
import sys import sys
import threading import threading
import time
import urllib.error import urllib.error
import urllib.request import urllib.request
@@ -316,43 +318,61 @@ def build_prompt(meta, files, diff_path, agent_md, impact_report_path=None):
return prompt return prompt
def run_claude(claude_bin, project_root, prompt, extra_dir=None): def run_claude(claude_bin, project_root, prompt, extra_dir=None, prompt_file=None):
"""Run claude -p, return stdout.""" """Run claude -p, return stdout. Prompt is fed via ``< file`` redirect."""
cmd = [ cmd = [claude_bin, "-p", "--permission-mode", "bypassPermissions"]
claude_bin, "-p",
"--permission-mode", "acceptEdits",
"--verbose",
]
if extra_dir: if extra_dir:
cmd += ["--add-dir", extra_dir] cmd += ["--add-dir", extra_dir]
cmd.append(prompt)
print(f"[review_pr] Running: {' '.join(cmd[:4])} ...", file=sys.stderr) # Write prompt to temp file, then feed via shell redirect to avoid arg-too-long
if not prompt_file:
prompt_file = os.path.join(project_root, ".reviews", "_prompt.txt")
os.makedirs(os.path.dirname(prompt_file), exist_ok=True)
with open(prompt_file, "w", encoding="utf-8") as f:
f.write(prompt)
shell_cmd = f"{shlex.join(cmd)} < {shlex.quote(prompt_file)}"
print(f"[review_pr] Command: {shell_cmd}", file=sys.stderr)
proc = subprocess.Popen( proc = subprocess.Popen(
cmd, shell_cmd,
cwd=project_root, cwd=project_root,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, text=True,
encoding="utf-8", encoding="utf-8",
shell=True,
) )
def _stream(pipe, label, dest_lines): def _stream(pipe, prefix, dest_lines):
for line in pipe: for line in pipe:
line = line.rstrip("\n") line = line.rstrip("\n")
if line:
print(f"[{label}] {line}", file=sys.stderr)
dest_lines.append(line) dest_lines.append(line)
if line.strip():
stderr_lines = [] print(f"[{prefix}] {line}", file=sys.stderr)
t = threading.Thread(target=_stream, args=(proc.stderr, "claude:stderr", stderr_lines))
t.start()
stdout_lines = [] stdout_lines = []
_stream(proc.stdout, "claude", stdout_lines) stderr_lines = []
t.join(timeout=10) heartbeat_stop = threading.Event()
proc.wait(timeout=600) def _heartbeat(start):
while not heartbeat_stop.is_set():
heartbeat_stop.wait(30)
if not heartbeat_stop.is_set():
elapsed = int(time.time() - start)
print(f"[review_pr] ... still running ({elapsed}s elapsed)", file=sys.stderr)
t_beat = threading.Thread(target=_heartbeat, args=(time.time(),))
t_beat.start()
t_out = threading.Thread(target=_stream, args=(proc.stdout, "claude", stdout_lines))
t_err = threading.Thread(target=_stream, args=(proc.stderr, "claude:stderr", stderr_lines))
t_out.start()
t_err.start()
t_out.join(timeout=600)
heartbeat_stop.set()
t_err.join(timeout=10)
t_beat.join(timeout=5)
proc.wait(timeout=10)
if proc.returncode != 0: if proc.returncode != 0:
print(f"Claude exited with code {proc.returncode}", file=sys.stderr) print(f"Claude exited with code {proc.returncode}", file=sys.stderr)
@@ -367,7 +387,7 @@ def extract_json_from_output(stdout):
match = re.search(r"```json\s*([\s\S]*)\s*```", stdout) match = re.search(r"```json\s*([\s\S]*)\s*```", stdout)
if not match: if not match:
print("ERROR: No ```json block found in Claude output.", file=sys.stderr) print("ERROR: No ```json block found in Claude output.", file=sys.stderr)
print(stdout[:3000], file=sys.stderr) print(stdout[-3000:], file=sys.stderr)
sys.exit(1) sys.exit(1)
try: try:
@@ -526,9 +546,11 @@ def main():
print(f" Diff: {len(diff_text)} chars, ~{diff_text.count(chr(10))} lines") print(f" Diff: {len(diff_text)} chars, ~{diff_text.count(chr(10))} lines")
# ── 4. Write diff inside project_root so claude CLI can read it ───── # ── 4. Write diff inside project_root so claude CLI can read it ─────
repo_slug = args.target_repo.replace("/", "_")
prefix = f"{repo_slug}_pr{args.pr}"
diff_dir = os.path.join(project_root, ".reviews") diff_dir = os.path.join(project_root, ".reviews")
os.makedirs(diff_dir, exist_ok=True) os.makedirs(diff_dir, exist_ok=True)
diff_path = os.path.join(diff_dir, f"pr{args.pr}.diff") diff_path = os.path.join(diff_dir, f"{prefix}.diff")
with open(diff_path, "w", encoding="utf-8") as f: with open(diff_path, "w", encoding="utf-8") as f:
f.write(diff_text) f.write(diff_text)
print(f" Diff saved to: {diff_path}") print(f" Diff saved to: {diff_path}")
@@ -544,18 +566,19 @@ def main():
symbols = _extract_symbols(diff_text) symbols = _extract_symbols(diff_text)
print(f"[review_pr] Extracted {len(symbols)} changed symbol(s): {', '.join(symbols) if symbols else '(none)'}") print(f"[review_pr] Extracted {len(symbols)} changed symbol(s): {', '.join(symbols) if symbols else '(none)'}")
report = _build_impact_report(repo_dir, changed_file_names, symbols) report = _build_impact_report(repo_dir, changed_file_names, symbols)
impact_report_path = os.path.join(diff_dir, f"pr{args.pr}_impact.md") impact_report_path = os.path.join(diff_dir, f"{prefix}_impact.md")
with open(impact_report_path, "w", encoding="utf-8") as f: with open(impact_report_path, "w", encoding="utf-8") as f:
f.write(report) f.write(report)
print(f" Impact report saved to: {impact_report_path}") print(f" Impact report saved to: {impact_report_path}")
else: else:
print("[review_pr] No --repo-dir provided — skipping global impact analysis") print("[review_pr] No --repo-dir provided — skipping global impact analysis")
prompt_file = os.path.join(diff_dir, f"{prefix}_prompt.txt")
try: try:
# ── 5. Build prompt & run Claude ───────────────────────────────── # ── 5. Build prompt & run Claude ─────────────────────────────────
prompt = build_prompt(meta, files, diff_path, agent_md, impact_report_path) prompt = build_prompt(meta, files, diff_path, agent_md, impact_report_path)
print("[review_pr] Invoking Claude for analysis...") print("[review_pr] Invoking Claude for analysis...")
stdout = run_claude(claude_bin, project_root, prompt, extra_dir=repo_dir) stdout = run_claude(claude_bin, project_root, prompt, extra_dir=repo_dir, prompt_file=prompt_file)
# ── 6. Parse Claude output ─────────────────────────────────────── # ── 6. Parse Claude output ───────────────────────────────────────
review = extract_json_from_output(stdout) review = extract_json_from_output(stdout)
@@ -573,6 +596,8 @@ def main():
# Clean up temp files # Clean up temp files
if os.path.exists(diff_path): if os.path.exists(diff_path):
os.unlink(diff_path) os.unlink(diff_path)
if os.path.exists(prompt_file):
os.unlink(prompt_file)
if impact_report_path and os.path.exists(impact_report_path): if impact_report_path and os.path.exists(impact_report_path):
os.unlink(impact_report_path) os.unlink(impact_report_path)