change stdout
This commit is contained in:
+49
-24
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user