

























@@ -240,7 +240,9 @@ def build_prompt(repo: Path, target: str, target_ref: str | None, bundle: str, e
240240 You are a senior code reviewer. Review the provided git change bundle only.
241241242242 Hard rules:
243- - Return JSON only, matching the supplied schema exactly.
243+ - Return exactly one JSON object and nothing else. Do not wrap it in Markdown.
244+ - The JSON object must match this schema exactly:
245+ {json.dumps(SCHEMA, indent=2)}
244246 - Do not modify files.
245247 - Do not invoke nested reviewers or review tools.
246248 - Forbidden nested review commands include: codex review, autoreview, claude review, oracle review.
@@ -331,6 +333,69 @@ def run_claude(args: argparse.Namespace, repo: Path, prompt: str) -> str:
331333return result.stdout
332334333335336+def run_droid(args: argparse.Namespace, repo: Path, prompt: str) -> str:
337+prompt_path = Path(tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False).name)
338+prompt_path.write_text(prompt)
339+cmd = [
340+args.droid_bin,
341+"exec",
342+"--cwd",
343+str(repo),
344+"--output-format",
345+"json",
346+"-f",
347+str(prompt_path),
348+ ]
349+if args.model:
350+cmd.extend(["--model", args.model])
351+if not args.tools:
352+cmd.extend(["--disabled-tools", "*"])
353+result = run(cmd, repo, check=False)
354+prompt_path.unlink(missing_ok=True)
355+if result.returncode != 0:
356+raise SystemExit(f"droid engine failed ({result.returncode})\n{result.stderr or result.stdout}")
357+return result.stdout
358+359+360+def run_copilot(args: argparse.Namespace, repo: Path, prompt: str) -> str:
361+if not args.tools:
362+raise SystemExit("--no-tools is not supported by the copilot engine; copilot requires a read-only file view tool to load the review bundle without exposing it in argv")
363+with tempfile.TemporaryDirectory(prefix="autoreview-copilot.") as tempdir:
364+prompt_path = Path(tempdir) / "prompt.txt"
365+prompt_path.write_text(prompt)
366+os.chmod(prompt_path, 0o600)
367+cmd = [
368+args.copilot_bin,
369+"-C",
370+tempdir,
371+"-p",
372+"Read ./prompt.txt and follow it exactly. Return only the requested JSON object.",
373+"--output-format",
374+"json",
375+"--stream",
376+"off",
377+"--no-ask-user",
378+"--disable-builtin-mcps",
379+ ]
380+if args.model:
381+cmd.extend(["--model", args.model])
382+cmd.extend(
383+ [
384+"--available-tools=read_agent,rg,view,web_fetch",
385+"--allow-tool=read_agent",
386+"--allow-tool=rg",
387+"--allow-tool=view",
388+"--allow-tool=web_fetch",
389+ ]
390+ )
391+if args.web_search:
392+cmd.append("--allow-all-urls")
393+result = run(cmd, Path(tempdir), check=False)
394+if result.returncode != 0:
395+raise SystemExit(f"copilot engine failed ({result.returncode})\n{result.stderr or result.stdout}")
396+return result.stdout
397+398+334399def claude_allowed_tools(args: argparse.Namespace) -> str:
335400tools = [tool.strip() for tool in args.claude_allowed_tools.split(",") if tool.strip()]
336401if not args.web_search:
@@ -345,21 +410,71 @@ def extract_json(text: str) -> dict[str, Any]:
345410try:
346411parsed = json.loads(stripped)
347412except json.JSONDecodeError as exc:
413+fenced_report = parse_json_candidate(stripped)
414+if isinstance(fenced_report, dict) and "findings" in fenced_report:
415+return fenced_report
416+jsonl_report = extract_json_from_jsonl(stripped)
417+if jsonl_report:
418+return jsonl_report
348419raise SystemExit(f"review engine returned non-JSON output: {exc}\n{stripped[:2000]}")
349420if isinstance(parsed, dict) and "findings" in parsed:
350421return parsed
351422if isinstance(parsed, dict) and isinstance(parsed.get("structured_output"), dict):
352423return parsed["structured_output"]
353424if isinstance(parsed, dict) and isinstance(parsed.get("result"), str):
354-try:
355-result_json = json.loads(parsed["result"])
356-except json.JSONDecodeError as exc:
357-raise SystemExit(f"review engine result was not structured JSON: {exc}\n{parsed['result'][:2000]}")
425+result_json = parse_json_candidate(parsed["result"])
358426if isinstance(result_json, dict) and "findings" in result_json:
359427return result_json
428+raise SystemExit(f"review engine result was not structured JSON:\n{parsed['result'][:2000]}")
429+jsonl_report = extract_json_from_jsonl(stripped)
430+if jsonl_report:
431+return jsonl_report
360432raise SystemExit(f"review engine returned unexpected JSON shape:\n{json.dumps(parsed)[:2000]}")
361433362434435+def extract_json_from_jsonl(text: str) -> dict[str, Any] | None:
436+candidates: list[str] = []
437+for line in text.splitlines():
438+line = line.strip()
439+if not line:
440+continue
441+try:
442+event = json.loads(line)
443+except json.JSONDecodeError:
444+continue
445+if not isinstance(event, dict):
446+continue
447+part = event.get("part")
448+if isinstance(part, dict) and isinstance(part.get("text"), str):
449+candidates.append(part["text"])
450+data = event.get("data")
451+if isinstance(data, dict) and isinstance(data.get("content"), str):
452+candidates.append(data["content"])
453+if isinstance(event.get("result"), str):
454+candidates.append(event["result"])
455+for candidate in reversed(candidates):
456+parsed = parse_json_candidate(candidate)
457+if isinstance(parsed, dict) and "findings" in parsed:
458+return parsed
459+return None
460+461+462+def parse_json_candidate(text: str) -> Any | None:
463+stripped = text.strip()
464+if stripped.startswith("```"):
465+lines = stripped.splitlines()
466+if lines and lines[0].startswith("```") and lines[-1].strip() == "```":
467+stripped = "\n".join(lines[1:-1]).strip()
468+try:
469+parsed = json.loads(stripped)
470+except json.JSONDecodeError:
471+return None
472+if isinstance(parsed, str) and parsed != text:
473+nested = parse_json_candidate(parsed)
474+return nested if nested is not None else parsed
475+return parsed
476+477+363478def validate_report(report: dict[str, Any], repo: Path, changed_paths: set[str], required: list[str]) -> None:
364479allowed_top = {"findings", "overall_correctness", "overall_explanation", "overall_confidence"}
365480extra_top = set(report) - allowed_top
@@ -459,11 +574,13 @@ def parse_args() -> argparse.Namespace:
459574parser.add_argument("--mode", choices=["auto", "local", "branch", "commit"], default="auto")
460575parser.add_argument("--base")
461576parser.add_argument("--commit", default="HEAD")
462-parser.add_argument("--engine", choices=["codex", "claude"], default=os.environ.get("AUTOREVIEW_ENGINE", "codex"))
577+parser.add_argument("--engine", choices=["codex", "claude", "droid", "copilot"], default=os.environ.get("AUTOREVIEW_ENGINE", "codex"))
463578parser.add_argument("--model")
464579parser.add_argument("--codex-bin", default=os.environ.get("CODEX_BIN", "codex"))
465580parser.add_argument("--claude-bin", default=os.environ.get("CLAUDE_BIN", "claude"))
466-parser.add_argument("--no-tools", dest="tools", action="store_false", default=True, help="Disable tools for engines that support it. Codex requires tools for review.")
581+parser.add_argument("--droid-bin", default=os.environ.get("DROID_BIN", "droid"))
582+parser.add_argument("--copilot-bin", default=os.environ.get("COPILOT_BIN", "copilot"))
583+parser.add_argument("--no-tools", dest="tools", action="store_false", default=True, help="Disable tools for engines that support it. Codex and copilot reject no-tools review.")
467584parser.add_argument("--no-web-search", dest="web_search", action="store_false", default=True)
468585parser.add_argument(
469586"--claude-allowed-tools",
@@ -482,11 +599,23 @@ def parse_args() -> argparse.Namespace:
482599parser.add_argument("--expect-findings", action="store_true", help="Treat findings as success; for harness acceptance tests.")
483600parser.add_argument("--dry-run", action="store_true")
484601args = parser.parse_args()
485-if args.engine not in {"codex", "claude"}:
602+if args.engine not in {"codex", "claude", "droid", "copilot"}:
486603raise SystemExit(f"invalid --engine/AUTOREVIEW_ENGINE: {args.engine}")
487604return args
488605489606607+def run_engine(args: argparse.Namespace, repo: Path, prompt: str) -> str:
608+if args.engine == "codex":
609+return run_codex(args, repo, prompt)
610+if args.engine == "claude":
611+return run_claude(args, repo, prompt)
612+if args.engine == "droid":
613+return run_droid(args, repo, prompt)
614+if args.engine == "copilot":
615+return run_copilot(args, repo, prompt)
616+raise SystemExit(f"unsupported engine: {args.engine}")
617+618+490619def main() -> int:
491620args = parse_args()
492621repo = repo_root()
@@ -518,7 +647,7 @@ def main() -> int:
518647if args.parallel_tests:
519648tests_proc = start_parallel_tests(args.parallel_tests, repo)
520649try:
521-raw = run_codex(args, repo, prompt) if args.engine == "codex" else run_claude(args, repo, prompt)
650+raw = run_engine(args, repo, prompt)
522651report = extract_json(raw)
523652validate_report(report, repo, changed_paths, args.require_finding)
524653if args.json_output:
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。