feat: tune reviewer for precision — web/wiki tools + recalibrated prompt (#1312)
* feat: tune reviewer for precision — web/wiki tools + recalibrated prompt
Reviewer agent now has web_search, fetch_url, and http_request alongside the
finding tools, so it can verify library semantics and consult the DeepWiki
auto-generated wiki for public repos (https://deepwiki.com/<owner>/<repo>)
before flagging cross-file or architectural concerns.
Prompt rewritten to push precision over recall:
- explicit severity ladder pushing reviews toward bimodal high/low instead of
defaulting to medium
- ≤200-char description target (gold set averages ~186 chars; we were at ~436)
- mandatory docs / wiki / code lookup before flagging concurrency, security,
or perf — the three categories that dominated false positives
- "do not flag" list covering compiler/linter-catchable nits, speculative
claims without a concrete attacker/interleaving/scale, style preferences
the codebase doesn't share, and test-quality nits on non-test diffs
- smart file-selection guidance for large PRs (deprioritize generated /
vendored / pure-rename hunks)
Eval config switched to openai:gpt-5.5 + high reasoning effort for the next
benchmark run.
* trim prompt
* subagent prompting
* confidence ratings
* added medium
* enforce confidence threshold
* .
* reviewer: precision-tuned prompt + drop confidence gate
Rewrites the reviewer system prompt around a defensibility bar (anchor +
failure mode + maintainer wouldn't say "not a bug"), an explicit do-not-file
list (style nits, speculation, scope-policing, same-bug fan-out), and a
checklist of 10 bug archetypes drawn from a per-PR audit of the eval golden
set. The audit showed 145 FPs in the last eval split ~28% speculative, ~26%
style-nit, ~31% real-but-unscored (mostly same-archetype fan-out); the new
prompt targets each class directly.
Confidence is still recorded on every finding for post-hoc calibration but
no longer gates publication — the audit showed the gate was a no-op (agent
self-rated 65% of findings "high" regardless), and the prompt's defensibility
bar is the actual discipline. Drops CONFIDENCE_ORDER, CONFIDENCE_THRESHOLD,
the confidence_threshold kwarg on filter_findings_for_publish, the
confidence_filtered score_mode, and the min_confidence kwarg on the eval
target's _extract_comments — all dead once the gate is gone.
Also removes the "informational" severity tier from the Severity enum,
SEVERITY_ORDER, and all validators / tests / docstrings. It was reserved for
FYI observations the dataset never rewards.
* benchmax
* adding google provider
* slight steering
* tuning
* more tuning
* fix
* cleanup
* reducing overfitting
* Add per-repo review style profiles and inject them into the reviewer.
Dashboard users can analyze historical PR review feedback per repository,
edit the resulting style guide, and have it loaded from LangGraph Store at
reviewer runtime (including Martian eval runs) keyed by owner/name.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix review style job errors leaking exception details to clients.
Return generic dashboard messages while logging full stack traces server-side.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 11:35:00 -07:00
|
|
|
"""Per-repository review style profiles in LangGraph Store.
|
|
|
|
|
|
|
|
|
|
Each record holds a synthesized custom prompt (editable in the dashboard),
|
|
|
|
|
analysis metadata, and the status of the background style-analysis run.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
|
from typing import Any, Literal
|
|
|
|
|
|
|
|
|
|
from langgraph_sdk import get_client
|
|
|
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
REVIEW_STYLES_NAMESPACE: list[str] = ["review_styles"]
|
|
|
|
|
|
|
|
|
|
AnalysisStatus = Literal["idle", "running", "completed", "failed"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def normalize_repo_full_name(raw: str) -> str:
|
|
|
|
|
"""Normalize user input to ``owner/repo``."""
|
|
|
|
|
v = raw.strip()
|
|
|
|
|
for prefix in ("https://github.com/", "http://github.com/", "github.com/"):
|
|
|
|
|
if v.lower().startswith(prefix):
|
|
|
|
|
v = v[len(prefix) :]
|
|
|
|
|
v = v.strip("/")
|
|
|
|
|
if v.endswith(".git"):
|
|
|
|
|
v = v[:-4]
|
|
|
|
|
parts = [p for p in v.split("/") if p]
|
|
|
|
|
if len(parts) != 2:
|
|
|
|
|
raise ValueError("full_name must be owner/repo")
|
|
|
|
|
return f"{parts[0]}/{parts[1]}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ReviewStyleCreate(BaseModel):
|
|
|
|
|
full_name: str = Field(..., description="GitHub repo in owner/name form")
|
|
|
|
|
|
|
|
|
|
@field_validator("full_name", mode="before")
|
|
|
|
|
@classmethod
|
|
|
|
|
def _valid_full_name(cls, v: str) -> str:
|
|
|
|
|
return normalize_repo_full_name(v)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ReviewStylePromptUpdate(BaseModel):
|
|
|
|
|
custom_prompt: str
|
|
|
|
|
|
|
|
|
|
@field_validator("custom_prompt")
|
|
|
|
|
@classmethod
|
|
|
|
|
def _non_empty(cls, v: str) -> str:
|
|
|
|
|
if not v.strip():
|
|
|
|
|
raise ValueError("custom_prompt cannot be empty")
|
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _client():
|
|
|
|
|
return get_client()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _get_value(key: str) -> dict[str, Any] | None:
|
|
|
|
|
try:
|
|
|
|
|
item = await _client().store.get_item(REVIEW_STYLES_NAMESPACE, key)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.debug("store get_item failed for %s: %s", key, e)
|
|
|
|
|
return None
|
|
|
|
|
if item is None:
|
|
|
|
|
return None
|
|
|
|
|
value = item.get("value") if isinstance(item, dict) else getattr(item, "value", None)
|
|
|
|
|
return value if isinstance(value, dict) else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _now_iso() -> str:
|
|
|
|
|
return datetime.now(UTC).isoformat()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _default_record(full_name: str, created_by: str) -> dict[str, Any]:
|
|
|
|
|
owner, name = full_name.split("/", 1)
|
|
|
|
|
return {
|
|
|
|
|
"full_name": full_name,
|
|
|
|
|
"owner": owner,
|
|
|
|
|
"name": name,
|
|
|
|
|
"status": "idle",
|
|
|
|
|
"custom_prompt": None,
|
|
|
|
|
"analysis_summary": None,
|
|
|
|
|
"top_reviewers": [],
|
|
|
|
|
"prs_sampled": 0,
|
|
|
|
|
"reviews_sampled": 0,
|
|
|
|
|
"analysis_thread_id": None,
|
|
|
|
|
"analysis_run_id": None,
|
|
|
|
|
"error": None,
|
|
|
|
|
"created_by": created_by,
|
|
|
|
|
"created_at": _now_iso(),
|
|
|
|
|
"updated_at": _now_iso(),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_review_style(full_name: str) -> dict[str, Any] | None:
|
|
|
|
|
return await _get_value(full_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def list_review_styles() -> list[dict[str, Any]]:
|
|
|
|
|
result = await _client().store.search_items(REVIEW_STYLES_NAMESPACE, limit=1000)
|
|
|
|
|
items = result.get("items") if isinstance(result, dict) else getattr(result, "items", [])
|
|
|
|
|
out: list[dict[str, Any]] = []
|
|
|
|
|
for item in items or []:
|
|
|
|
|
value = item.get("value") if isinstance(item, dict) else getattr(item, "value", None)
|
|
|
|
|
if isinstance(value, dict):
|
|
|
|
|
out.append(value)
|
|
|
|
|
out.sort(key=lambda r: r.get("full_name", ""))
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def create_review_style(full_name: str, created_by: str) -> dict[str, Any]:
|
|
|
|
|
existing = await get_review_style(full_name)
|
|
|
|
|
if existing:
|
|
|
|
|
return existing
|
|
|
|
|
value = _default_record(full_name, created_by)
|
|
|
|
|
await _client().store.put_item(REVIEW_STYLES_NAMESPACE, full_name, value)
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def update_review_style(full_name: str, patch: dict[str, Any]) -> dict[str, Any]:
|
|
|
|
|
existing = await get_review_style(full_name) or _default_record(
|
|
|
|
|
full_name, patch.get("created_by", "")
|
|
|
|
|
)
|
|
|
|
|
value = {**existing, **patch, "updated_at": _now_iso()}
|
|
|
|
|
await _client().store.put_item(REVIEW_STYLES_NAMESPACE, full_name, value)
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
2026-05-21 10:44:04 -07:00
|
|
|
def has_saved_prompt(record: dict[str, Any]) -> bool:
|
|
|
|
|
prompt = record.get("custom_prompt")
|
|
|
|
|
return isinstance(prompt, str) and bool(prompt.strip())
|
|
|
|
|
|
|
|
|
|
|
feat: tune reviewer for precision — web/wiki tools + recalibrated prompt (#1312)
* feat: tune reviewer for precision — web/wiki tools + recalibrated prompt
Reviewer agent now has web_search, fetch_url, and http_request alongside the
finding tools, so it can verify library semantics and consult the DeepWiki
auto-generated wiki for public repos (https://deepwiki.com/<owner>/<repo>)
before flagging cross-file or architectural concerns.
Prompt rewritten to push precision over recall:
- explicit severity ladder pushing reviews toward bimodal high/low instead of
defaulting to medium
- ≤200-char description target (gold set averages ~186 chars; we were at ~436)
- mandatory docs / wiki / code lookup before flagging concurrency, security,
or perf — the three categories that dominated false positives
- "do not flag" list covering compiler/linter-catchable nits, speculative
claims without a concrete attacker/interleaving/scale, style preferences
the codebase doesn't share, and test-quality nits on non-test diffs
- smart file-selection guidance for large PRs (deprioritize generated /
vendored / pure-rename hunks)
Eval config switched to openai:gpt-5.5 + high reasoning effort for the next
benchmark run.
* trim prompt
* subagent prompting
* confidence ratings
* added medium
* enforce confidence threshold
* .
* reviewer: precision-tuned prompt + drop confidence gate
Rewrites the reviewer system prompt around a defensibility bar (anchor +
failure mode + maintainer wouldn't say "not a bug"), an explicit do-not-file
list (style nits, speculation, scope-policing, same-bug fan-out), and a
checklist of 10 bug archetypes drawn from a per-PR audit of the eval golden
set. The audit showed 145 FPs in the last eval split ~28% speculative, ~26%
style-nit, ~31% real-but-unscored (mostly same-archetype fan-out); the new
prompt targets each class directly.
Confidence is still recorded on every finding for post-hoc calibration but
no longer gates publication — the audit showed the gate was a no-op (agent
self-rated 65% of findings "high" regardless), and the prompt's defensibility
bar is the actual discipline. Drops CONFIDENCE_ORDER, CONFIDENCE_THRESHOLD,
the confidence_threshold kwarg on filter_findings_for_publish, the
confidence_filtered score_mode, and the min_confidence kwarg on the eval
target's _extract_comments — all dead once the gate is gone.
Also removes the "informational" severity tier from the Severity enum,
SEVERITY_ORDER, and all validators / tests / docstrings. It was reserved for
FYI observations the dataset never rewards.
* benchmax
* adding google provider
* slight steering
* tuning
* more tuning
* fix
* cleanup
* reducing overfitting
* Add per-repo review style profiles and inject them into the reviewer.
Dashboard users can analyze historical PR review feedback per repository,
edit the resulting style guide, and have it loaded from LangGraph Store at
reviewer runtime (including Martian eval runs) keyed by owner/name.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix review style job errors leaking exception details to clients.
Return generic dashboard messages while logging full stack traces server-side.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 11:35:00 -07:00
|
|
|
async def set_custom_prompt(full_name: str, custom_prompt: str) -> dict[str, Any]:
|
2026-05-21 10:44:04 -07:00
|
|
|
existing = await get_review_style(full_name)
|
|
|
|
|
patch: dict[str, Any] = {"custom_prompt": custom_prompt}
|
|
|
|
|
if existing and existing.get("status") == "running":
|
|
|
|
|
patch["status"] = "completed"
|
|
|
|
|
patch["error"] = None
|
|
|
|
|
return await update_review_style(full_name, patch)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def reconcile_running_status(
|
|
|
|
|
full_name: str,
|
|
|
|
|
record: dict[str, Any],
|
|
|
|
|
*,
|
|
|
|
|
run_status: str | None,
|
|
|
|
|
run_missing: bool = False,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
"""Clear stale ``running`` when the analyzer run is done or unreachable."""
|
|
|
|
|
if record.get("status") != "running":
|
|
|
|
|
return record
|
|
|
|
|
|
|
|
|
|
terminal_success = frozenset({"success", "completed"})
|
|
|
|
|
terminal_failure = frozenset({"error", "failed", "timeout", "interrupted", "cancelled"})
|
|
|
|
|
|
|
|
|
|
if run_status in terminal_success:
|
|
|
|
|
if has_saved_prompt(record):
|
|
|
|
|
return await update_review_style(full_name, {"status": "completed", "error": None})
|
|
|
|
|
return await mark_analysis_failed(
|
|
|
|
|
full_name,
|
|
|
|
|
"Analysis finished without saving a prompt. Please retry.",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if run_status in terminal_failure:
|
|
|
|
|
if has_saved_prompt(record):
|
|
|
|
|
return await update_review_style(full_name, {"status": "completed", "error": None})
|
|
|
|
|
return await mark_analysis_failed(full_name, "Analysis run ended. Please retry.")
|
|
|
|
|
|
|
|
|
|
if run_missing:
|
|
|
|
|
if has_saved_prompt(record):
|
|
|
|
|
return await update_review_style(full_name, {"status": "completed", "error": None})
|
|
|
|
|
return await mark_analysis_failed(
|
|
|
|
|
full_name,
|
|
|
|
|
"Analysis was interrupted or the run is no longer available. Please retry.",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return record
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def delete_review_style(full_name: str) -> None:
|
|
|
|
|
await _client().store.delete_item(REVIEW_STYLES_NAMESPACE, full_name)
|
feat: tune reviewer for precision — web/wiki tools + recalibrated prompt (#1312)
* feat: tune reviewer for precision — web/wiki tools + recalibrated prompt
Reviewer agent now has web_search, fetch_url, and http_request alongside the
finding tools, so it can verify library semantics and consult the DeepWiki
auto-generated wiki for public repos (https://deepwiki.com/<owner>/<repo>)
before flagging cross-file or architectural concerns.
Prompt rewritten to push precision over recall:
- explicit severity ladder pushing reviews toward bimodal high/low instead of
defaulting to medium
- ≤200-char description target (gold set averages ~186 chars; we were at ~436)
- mandatory docs / wiki / code lookup before flagging concurrency, security,
or perf — the three categories that dominated false positives
- "do not flag" list covering compiler/linter-catchable nits, speculative
claims without a concrete attacker/interleaving/scale, style preferences
the codebase doesn't share, and test-quality nits on non-test diffs
- smart file-selection guidance for large PRs (deprioritize generated /
vendored / pure-rename hunks)
Eval config switched to openai:gpt-5.5 + high reasoning effort for the next
benchmark run.
* trim prompt
* subagent prompting
* confidence ratings
* added medium
* enforce confidence threshold
* .
* reviewer: precision-tuned prompt + drop confidence gate
Rewrites the reviewer system prompt around a defensibility bar (anchor +
failure mode + maintainer wouldn't say "not a bug"), an explicit do-not-file
list (style nits, speculation, scope-policing, same-bug fan-out), and a
checklist of 10 bug archetypes drawn from a per-PR audit of the eval golden
set. The audit showed 145 FPs in the last eval split ~28% speculative, ~26%
style-nit, ~31% real-but-unscored (mostly same-archetype fan-out); the new
prompt targets each class directly.
Confidence is still recorded on every finding for post-hoc calibration but
no longer gates publication — the audit showed the gate was a no-op (agent
self-rated 65% of findings "high" regardless), and the prompt's defensibility
bar is the actual discipline. Drops CONFIDENCE_ORDER, CONFIDENCE_THRESHOLD,
the confidence_threshold kwarg on filter_findings_for_publish, the
confidence_filtered score_mode, and the min_confidence kwarg on the eval
target's _extract_comments — all dead once the gate is gone.
Also removes the "informational" severity tier from the Severity enum,
SEVERITY_ORDER, and all validators / tests / docstrings. It was reserved for
FYI observations the dataset never rewards.
* benchmax
* adding google provider
* slight steering
* tuning
* more tuning
* fix
* cleanup
* reducing overfitting
* Add per-repo review style profiles and inject them into the reviewer.
Dashboard users can analyze historical PR review feedback per repository,
edit the resulting style guide, and have it loaded from LangGraph Store at
reviewer runtime (including Martian eval runs) keyed by owner/name.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix review style job errors leaking exception details to clients.
Return generic dashboard messages while logging full stack traces server-side.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 11:35:00 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
async def mark_analysis_running(
|
|
|
|
|
full_name: str,
|
|
|
|
|
*,
|
|
|
|
|
thread_id: str,
|
|
|
|
|
run_id: str | None,
|
|
|
|
|
top_reviewers: list[str],
|
|
|
|
|
prs_sampled: int,
|
|
|
|
|
reviews_sampled: int,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return await update_review_style(
|
|
|
|
|
full_name,
|
|
|
|
|
{
|
|
|
|
|
"status": "running",
|
|
|
|
|
"analysis_thread_id": thread_id,
|
|
|
|
|
"analysis_run_id": run_id,
|
|
|
|
|
"top_reviewers": top_reviewers,
|
|
|
|
|
"prs_sampled": prs_sampled,
|
|
|
|
|
"reviews_sampled": reviews_sampled,
|
|
|
|
|
"error": None,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def mark_analysis_completed(
|
|
|
|
|
full_name: str,
|
|
|
|
|
*,
|
|
|
|
|
custom_prompt: str,
|
|
|
|
|
analysis_summary: str,
|
|
|
|
|
top_reviewers: list[str],
|
|
|
|
|
prs_sampled: int,
|
|
|
|
|
reviews_sampled: int,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return await update_review_style(
|
|
|
|
|
full_name,
|
|
|
|
|
{
|
|
|
|
|
"status": "completed",
|
|
|
|
|
"custom_prompt": custom_prompt,
|
|
|
|
|
"analysis_summary": analysis_summary,
|
|
|
|
|
"top_reviewers": top_reviewers,
|
|
|
|
|
"prs_sampled": prs_sampled,
|
|
|
|
|
"reviews_sampled": reviews_sampled,
|
|
|
|
|
"error": None,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def mark_analysis_failed(full_name: str, error: str) -> dict[str, Any]:
|
|
|
|
|
return await update_review_style(full_name, {"status": "failed", "error": error})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_repo_custom_prompt(owner: str, repo: str) -> str | None:
|
|
|
|
|
"""Return the custom prompt supplement for a repo, if configured."""
|
|
|
|
|
full_name = f"{owner}/{repo}"
|
|
|
|
|
record = await get_review_style(full_name)
|
|
|
|
|
if not record:
|
|
|
|
|
return None
|
|
|
|
|
prompt = record.get("custom_prompt")
|
|
|
|
|
if isinstance(prompt, str) and prompt.strip():
|
|
|
|
|
return prompt.strip()
|
|
|
|
|
return None
|