mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
feat(autoreview): support cursor-agent engine
This commit is contained in:
@@ -29,7 +29,7 @@ Use when:
|
||||
- For security-audit suppression changes, verify accepted findings remain auditable: suppressed findings stay in structured output, active output keeps an unsuppressible suppression notice, and aggregate findings cannot hide unrelated active risk.
|
||||
- Never switch or override the requested review engine/model. If the review hits model capacity, retry the same command a few times with the same engine/model.
|
||||
- Be patient with large bundles. Structured review can take up to 30 minutes while the model call is active, especially with Codex tools or web search.
|
||||
- Treat heartbeat lines like `review still running: ... elapsed=... pid=...` as healthy progress, not a hang. Let the helper continue while heartbeats are advancing. Pass `--stream-engine-output` when live engine text is useful; Codex and Claude filter tool/file chatter, other engines pass raw output through.
|
||||
- Treat heartbeat lines like `review still running: ... elapsed=... pid=...` as healthy progress, not a hang. Let the helper continue while heartbeats are advancing. Pass `--stream-engine-output` when live engine text is useful; Codex, Claude, and cursor-agent filter tool/file chatter, other engines pass raw output through.
|
||||
- Do not kill a review just because it has been quiet for 2-5 minutes, or because it is still running under the 30-minute window. Inspect the process only after missing multiple expected heartbeats, after 30 minutes, or after an obviously failed subprocess; prefer letting the same helper command finish.
|
||||
- Tools are useful in review mode. The helper allows read-only inspection tools and web search by default so reviewers can check dependency contracts, upstream docs, and current behavior.
|
||||
- Security perspective is always included, but it should not cripple legitimate functionality. Report security findings only when the change creates a concrete, actionable risk or removes an important safety check.
|
||||
@@ -226,15 +226,16 @@ The helper:
|
||||
- accepts `--mode uncommitted` as an alias for `--mode local`
|
||||
- otherwise uses current PR base if `gh pr view` works
|
||||
- otherwise uses `origin/main` for non-main branches
|
||||
- supports `--engine codex`, `claude`, `droid`, and `copilot`; default is `AUTOREVIEW_ENGINE` or `codex`; Codex should remain the default when nothing is set
|
||||
- supports `--engine codex`, `claude`, `droid`, `copilot`, and `cursor-agent`; default is `AUTOREVIEW_ENGINE` or `codex`; Codex should remain the default when nothing is set
|
||||
- resolves bare `git`, `gh`, reviewer, and PowerShell shell commands from absolute `PATH` entries only, never from the reviewed checkout; explicit relative `--*-bin` paths are resolved from the reviewed repository root
|
||||
- use `--mode commit --commit <ref>` for already-committed work, especially clean `main` after landing
|
||||
- should be left in `--mode auto` or forced to `--mode branch` for PR/branch work; do not force `--mode local` after committing
|
||||
- writes only to stdout unless `--output`, `--json-output`, or live streamed engine stderr is set
|
||||
- supports `--dry-run`, `--parallel-tests`, `--parallel-tests-shell`, `--prompt`, `--prompt-file`, `--dataset`, `--no-tools`, `--no-web-search`, and commit refs
|
||||
- supports `--stream-engine-output` or `AUTOREVIEW_STREAM_ENGINE_OUTPUT=1` for live engine text while preserving structured validation; Codex and Claude hide tool/file event details, emit compact activity summaries, and report usage at turn completion
|
||||
- supports `--stream-engine-output` or `AUTOREVIEW_STREAM_ENGINE_OUTPUT=1` for live engine text while preserving structured validation; Codex, Claude, and cursor-agent hide tool/file event details, emit compact activity summaries, and report usage at turn completion
|
||||
- supports opt-in review panels with `--panel` / `--reviewers`, plus per-engine `--model` and `--thinking`
|
||||
- allows read-only tools and web search by default where the selected CLI supports them; forbids nested review in the prompt; Codex is run through `codex exec` with read-only sandbox and structured output
|
||||
- allows read-only tools and web search by default where the selected CLI supports them; forbids nested review in the prompt; Codex is run through `codex exec` with read-only sandbox and structured output; cursor-agent is run through headless `--print` in ask mode with sandboxing enabled from a helper-owned temporary workspace
|
||||
- rejects `--no-web-search` for cursor-agent because the Cursor CLI does not expose a CLI-level web-search disable switch
|
||||
- prints `review still running: <engine> elapsed=<seconds>s pid=<pid>` to stderr at long-running intervals while waiting for the selected review engine, unless streamed output or compact Codex activity has been visible recently
|
||||
- prints `autoreview clean: no accepted/actionable findings reported` when the selected review command exits 0
|
||||
- exits nonzero when accepted/actionable findings are present
|
||||
|
||||
@@ -17,12 +17,13 @@ from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
ENGINES = ("codex", "claude", "droid", "copilot")
|
||||
ENGINES = ("codex", "claude", "droid", "copilot", "cursor-agent")
|
||||
THINKING_LEVELS_BY_ENGINE = {
|
||||
"codex": {"low", "medium", "high", "xhigh"},
|
||||
"claude": {"low", "medium", "high", "xhigh", "max"},
|
||||
"droid": set(),
|
||||
"copilot": set(),
|
||||
"cursor-agent": set(),
|
||||
}
|
||||
|
||||
|
||||
@@ -480,7 +481,7 @@ def build_prompt(repo: Path, target: str, target_ref: str | None, bundle: str, e
|
||||
{json.dumps(SCHEMA, indent=2)}
|
||||
- Do not modify files.
|
||||
- Do not invoke nested reviewers or review tools.
|
||||
- Forbidden nested review commands include: codex review, autoreview, claude review, oracle review.
|
||||
- Forbidden nested review commands include: codex review, autoreview, claude review, cursor-agent, oracle review.
|
||||
- You may use read-only tools and web search to inspect files, dependency contracts, upstream docs, current behavior, and security implications.
|
||||
- Shell commands, if available, must be read-only inspection commands. Do not run tests, formatters, package installs, generators, network mutation commands, git mutation commands, or commands that write files.
|
||||
- Report only actionable defects introduced or exposed by this change.
|
||||
@@ -660,6 +661,44 @@ def run_copilot(args: argparse.Namespace, repo: Path, prompt: str) -> str:
|
||||
return result.stdout
|
||||
|
||||
|
||||
def run_cursor_agent(args: argparse.Namespace, repo: Path, prompt: str) -> str:
|
||||
if args.thinking:
|
||||
raise SystemExit("--thinking is not supported by the cursor-agent engine")
|
||||
if not args.tools:
|
||||
raise SystemExit("--no-tools is not supported by the cursor-agent engine; use --engine claude --no-tools for a no-tools run")
|
||||
if not args.web_search:
|
||||
raise SystemExit("--no-web-search is not supported by the cursor-agent engine; use an engine with a CLI-level web-search disable switch")
|
||||
with tempfile.TemporaryDirectory(prefix="autoreview-cursor-agent.") as tempdir:
|
||||
# Trust only the helper-owned empty workspace, never the reviewed repo.
|
||||
# Cursor may load trusted project hooks/config before model instructions apply.
|
||||
cmd = [
|
||||
resolve_command(args.cursor_agent_bin, repo),
|
||||
"--print",
|
||||
"--output-format",
|
||||
"stream-json" if args.stream_engine_output else "json",
|
||||
"--trust",
|
||||
"--workspace",
|
||||
tempdir,
|
||||
"--mode",
|
||||
"ask",
|
||||
"--sandbox",
|
||||
"enabled",
|
||||
]
|
||||
if args.model:
|
||||
cmd.extend(["--model", args.model])
|
||||
result = run_with_heartbeat(
|
||||
cmd,
|
||||
Path(tempdir),
|
||||
input_text=prompt,
|
||||
label="cursor-agent",
|
||||
stream_output=args.stream_engine_output,
|
||||
stream_display=CursorAgentStreamDisplay() if args.stream_engine_output else None,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(f"cursor-agent engine failed ({result.returncode})\n{result.stderr or result.stdout}")
|
||||
return result.stdout
|
||||
|
||||
|
||||
class CodexStreamDisplay:
|
||||
def __init__(self, *, activity_seconds: int = 20) -> None:
|
||||
self.activity_seconds = activity_seconds
|
||||
@@ -779,6 +818,41 @@ class ClaudeStreamDisplay:
|
||||
return text
|
||||
|
||||
|
||||
class CursorAgentStreamDisplay(ClaudeStreamDisplay):
|
||||
def __call__(self, name: str, line: str) -> str | None:
|
||||
if name != "stdout":
|
||||
return line
|
||||
try:
|
||||
event = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
return self.visible(line)
|
||||
event_type = event.get("type")
|
||||
if event_type == "system":
|
||||
return self.visible(f"cursor-agent session: {event.get('session_id', '<unknown>')}\n")
|
||||
if event_type == "assistant":
|
||||
return self.assistant_message(event)
|
||||
if event_type == "result":
|
||||
return self.visible(self.flush_hidden() + self.result_summary(event))
|
||||
return self.hidden_activity()
|
||||
|
||||
def result_summary(self, event: dict[str, Any]) -> str:
|
||||
usage = event.get("usage")
|
||||
fields: list[str] = []
|
||||
if isinstance(usage, dict):
|
||||
for key in ("inputTokens", "cacheReadTokens", "cacheWriteTokens", "outputTokens"):
|
||||
value = usage.get(key)
|
||||
if isinstance(value, int):
|
||||
fields.append(f"{key}={value}")
|
||||
return "cursor-agent usage: " + " ".join(fields) + "\n" if fields else "cursor-agent turn completed\n"
|
||||
|
||||
def flush_hidden(self) -> str:
|
||||
if not self.hidden_events:
|
||||
return ""
|
||||
count = self.hidden_events
|
||||
self.hidden_events = 0
|
||||
return f"cursor-agent activity: {count} hidden tool/status events\n"
|
||||
|
||||
|
||||
def format_codex_usage(usage: dict[str, Any]) -> str:
|
||||
fields = [
|
||||
"input_tokens",
|
||||
@@ -868,13 +942,35 @@ def parse_json_candidate(text: str) -> Any | None:
|
||||
try:
|
||||
parsed = json.loads(stripped)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
return parse_embedded_json_object(stripped)
|
||||
if isinstance(parsed, str) and parsed != text:
|
||||
nested = parse_json_candidate(parsed)
|
||||
return nested if nested is not None else parsed
|
||||
return parsed
|
||||
|
||||
|
||||
def parse_embedded_json_object(text: str) -> Any | None:
|
||||
decoder = json.JSONDecoder()
|
||||
candidates: list[Any] = []
|
||||
for index, char in enumerate(text):
|
||||
if char not in "[{":
|
||||
continue
|
||||
try:
|
||||
parsed, _end = decoder.raw_decode(text[index:])
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(parsed, str):
|
||||
nested = parse_json_candidate(parsed)
|
||||
if nested is not None:
|
||||
candidates.append(nested)
|
||||
else:
|
||||
candidates.append(parsed)
|
||||
for candidate in reversed(candidates):
|
||||
if isinstance(candidate, dict) and "findings" in candidate:
|
||||
return candidate
|
||||
return candidates[-1] if candidates else None
|
||||
|
||||
|
||||
def validate_report(report: dict[str, Any], repo: Path, changed_paths: set[str], required: list[str]) -> None:
|
||||
allowed_top = {"findings", "overall_correctness", "overall_explanation", "overall_confidence"}
|
||||
extra_top = set(report) - allowed_top
|
||||
@@ -1016,7 +1112,8 @@ def parse_args() -> argparse.Namespace:
|
||||
parser.add_argument("--claude-bin", default=os.environ.get("CLAUDE_BIN", "claude"))
|
||||
parser.add_argument("--droid-bin", default=os.environ.get("DROID_BIN", "droid"))
|
||||
parser.add_argument("--copilot-bin", default=os.environ.get("COPILOT_BIN", "copilot"))
|
||||
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.")
|
||||
parser.add_argument("--cursor-agent-bin", default=os.environ.get("CURSOR_AGENT_BIN", "cursor-agent"))
|
||||
parser.add_argument("--no-tools", dest="tools", action="store_false", default=True, help="Disable tools for engines that support it. Codex, copilot, and cursor-agent reject no-tools review.")
|
||||
parser.add_argument("--no-web-search", dest="web_search", action="store_false", default=True)
|
||||
parser.add_argument(
|
||||
"--claude-allowed-tools",
|
||||
@@ -1034,7 +1131,7 @@ def parse_args() -> argparse.Namespace:
|
||||
"--stream-engine-output",
|
||||
action="store_true",
|
||||
default=os.environ.get("AUTOREVIEW_STREAM_ENGINE_OUTPUT") == "1",
|
||||
help="Stream review engine output while preserving buffered output for validation. Codex output is filtered to hide tool/file chatter.",
|
||||
help="Stream review engine output while preserving buffered output for validation. Codex, Claude, and cursor-agent output is filtered to hide tool/file chatter.",
|
||||
)
|
||||
parser.add_argument("--parallel-tests", help="Run a test command concurrently with review; failure fails the helper.")
|
||||
parser.add_argument(
|
||||
@@ -1061,6 +1158,8 @@ def run_engine(args: argparse.Namespace, repo: Path, prompt: str) -> str:
|
||||
return run_droid(args, repo, prompt)
|
||||
if args.engine == "copilot":
|
||||
return run_copilot(args, repo, prompt)
|
||||
if args.engine == "cursor-agent":
|
||||
return run_cursor_agent(args, repo, prompt)
|
||||
raise SystemExit(f"unsupported engine: {args.engine}")
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ param(
|
||||
[ValidateSet('malicious', 'benign')]
|
||||
[string] $Fixture,
|
||||
|
||||
[ValidateSet('codex', 'claude', 'droid', 'copilot')]
|
||||
[ValidateSet('codex', 'claude', 'droid', 'copilot', 'cursor-agent')]
|
||||
[string[]] $Engine,
|
||||
|
||||
[Alias('h')]
|
||||
|
||||
@@ -13,7 +13,7 @@ from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ENGINES = ("codex", "claude", "droid", "copilot")
|
||||
ENGINES = ("codex", "claude", "droid", "copilot", "cursor-agent")
|
||||
DEFAULT_ENGINES = ("codex", "claude")
|
||||
|
||||
MALICIOUS_INITIAL = """export function uploadPath(name) {
|
||||
|
||||
Reference in New Issue
Block a user