change stdout
This commit is contained in:
+86
-23
@@ -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):
|
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 = [
|
cmd = [
|
||||||
claude_bin, "-p",
|
claude_bin, "-p",
|
||||||
|
"--output-format", "stream-json",
|
||||||
|
"--include-partial-messages",
|
||||||
"--permission-mode", "acceptEdits",
|
"--permission-mode", "acceptEdits",
|
||||||
"--verbose",
|
"--verbose",
|
||||||
]
|
]
|
||||||
@@ -327,7 +329,7 @@ def run_claude(claude_bin, project_root, prompt, extra_dir=None):
|
|||||||
cmd += ["--add-dir", extra_dir]
|
cmd += ["--add-dir", extra_dir]
|
||||||
cmd.append(prompt)
|
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(
|
proc = subprocess.Popen(
|
||||||
cmd,
|
cmd,
|
||||||
cwd=project_root,
|
cwd=project_root,
|
||||||
@@ -340,9 +342,33 @@ def run_claude(claude_bin, project_root, prompt, extra_dir=None):
|
|||||||
def _stream(pipe, label, dest_lines):
|
def _stream(pipe, label, dest_lines):
|
||||||
for line in pipe:
|
for line in pipe:
|
||||||
line = line.rstrip("\n")
|
line = line.rstrip("\n")
|
||||||
if line:
|
if not line:
|
||||||
print(f"[{label}] {line}", file=sys.stderr)
|
continue
|
||||||
dest_lines.append(line)
|
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 = []
|
stderr_lines = []
|
||||||
t = threading.Thread(target=_stream, args=(proc.stderr, "claude: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):
|
def extract_json_from_output(stdout):
|
||||||
"""Extract the review JSON from Claude's output."""
|
"""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)
|
|
||||||
|
|
||||||
|
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:
|
try:
|
||||||
obj = json.loads(match.group(1))
|
obj = json.loads(match.group(1))
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError:
|
||||||
print(f"ERROR: Invalid JSON in Claude output: {e}", file=sys.stderr)
|
return None
|
||||||
print(match.group(1)[:2000], file=sys.stderr)
|
if not isinstance(obj, dict):
|
||||||
sys.exit(1)
|
return None
|
||||||
|
|
||||||
# Unwrap {review: {...}} envelope if present
|
# 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"]
|
obj = obj["review"]
|
||||||
|
if "body" not in obj:
|
||||||
if not isinstance(obj, dict) or "body" not in obj:
|
return None
|
||||||
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)
|
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user