feat(robomp): added incoming PR review feature with classify and submit
- Added `review_pr` task that checks out PR head in a detached worktree, classifies rank/type/area, and posts a batched GitHub review as `event=COMMENT`. - Added four new host tools: `fetch_pr`, `classify_pr`, `pr_review_comment`, and `submit_pr_review`; review tools self-gate on `review_mode`, push/open-PR tools refuse when `review_mode` is set. - Added sqlite staging table `pr_review_comments` with `stage_review_comment`, `list_staged_review_comments`, and `clear_staged_review_comments` DAOs. - Routed `pull_request.opened/reopened/ready_for_review` to `review_pr` and extended `pull_request.closed` cleanup to any tracked PR regardless of author.
This commit is contained in:
@@ -290,6 +290,83 @@ async def triage_issue(
|
||||
await run_task(task_kind="triage_issue", inputs=inputs)
|
||||
|
||||
|
||||
async def review_pr(
|
||||
*,
|
||||
settings: Settings,
|
||||
db: Database,
|
||||
github: GitHubBackend,
|
||||
sandbox: SandboxManager,
|
||||
git_transport: GitTransport,
|
||||
payload: Mapping[str, Any],
|
||||
delivery_id: str,
|
||||
attempts: int = 0,
|
||||
slot_uid: int | None = None,
|
||||
) -> None:
|
||||
pr_node = payload.get("pull_request") or {}
|
||||
pr_number = int(pr_node.get("number") or 0)
|
||||
repo_payload = payload.get("repository") or {}
|
||||
repo_full = str(repo_payload.get("full_name") or "")
|
||||
if pr_number <= 0 or not repo_full:
|
||||
log.info("skip: review_pr missing repo/number")
|
||||
return
|
||||
try:
|
||||
repo = await github.get_repo(repo_full)
|
||||
issue = await github.get_issue(repo_full, pr_number)
|
||||
pr = await github.get_pull_request(repo_full, pr_number)
|
||||
except GitHubError as exc:
|
||||
log.warning("review_pr fetch failed", extra={"repo": repo_full, "pr": pr_number, "err": str(exc)})
|
||||
return
|
||||
|
||||
labels = {label.lower() for label in issue.labels}
|
||||
key = issue_key(repo.full_name, pr_number)
|
||||
review_labeled = "triaged" in labels or any(label.startswith("review:") for label in labels)
|
||||
if db.has_successful_tool_call(key, "submit_pr_review"):
|
||||
log.info("skip: PR review already submitted", extra={"repo": repo_full, "pr": pr_number})
|
||||
return
|
||||
if review_labeled:
|
||||
log.info(
|
||||
"review labels present without submitted review; retrying",
|
||||
extra={"repo": repo_full, "pr": pr_number, "labels": sorted(labels)},
|
||||
)
|
||||
|
||||
db.upsert_issue(key=key, repo=repo.full_name, number=pr_number, state="reviewing", pr_number=pr_number)
|
||||
workspace = sandbox.ensure_workspace(
|
||||
repo=repo.full_name,
|
||||
number=pr_number,
|
||||
title=issue.title,
|
||||
clone_url=repo.clone_url,
|
||||
default_branch=repo.default_branch,
|
||||
pr_head=pr_number,
|
||||
author_name=settings.resolved_author_name,
|
||||
author_email=settings.git_author_email,
|
||||
slot_uid=slot_uid,
|
||||
)
|
||||
db.upsert_issue(
|
||||
key=key,
|
||||
repo=repo.full_name,
|
||||
number=pr_number,
|
||||
state="reviewing",
|
||||
branch=workspace.branch,
|
||||
session_dir=str(workspace.session_dir),
|
||||
pr_number=pr_number,
|
||||
)
|
||||
inputs = TaskInputs(
|
||||
settings=settings,
|
||||
db=db,
|
||||
github=github,
|
||||
git_transport=git_transport,
|
||||
repo=repo,
|
||||
issue=issue,
|
||||
workspace=workspace,
|
||||
delivery_id=delivery_id,
|
||||
attempts=attempts,
|
||||
slot_uid=slot_uid,
|
||||
natives_cache=sandbox.natives_cache,
|
||||
)
|
||||
await run_task(task_kind="review_pr", inputs=inputs, pr_number=pr_number, pr=pr)
|
||||
return
|
||||
|
||||
|
||||
async def handle_comment(
|
||||
*,
|
||||
settings: Settings,
|
||||
@@ -566,6 +643,9 @@ async def handle_pr_conversation(
|
||||
if pr_info is None or not _can_handle_pr_directly(settings=settings, repo_full=repo_full, pr=pr_info):
|
||||
return
|
||||
directive = _directive_from_payload(payload)
|
||||
if issue_row is not None and issue_row.state == "reviewing":
|
||||
log.info("skip: incoming PR conversation unsupported", extra={"key": issue_row.key, "pr": pr_number})
|
||||
return
|
||||
if issue_row is not None and issue_row.state in ("merged", "closed", "abandoned"):
|
||||
if directive is None:
|
||||
log.info("skip: pr-conversation on finalized issue", extra={"key": issue_row.key, "state": issue_row.state})
|
||||
@@ -716,5 +796,6 @@ __all__ = [
|
||||
"handle_comment",
|
||||
"handle_pr_conversation",
|
||||
"handle_review",
|
||||
"review_pr",
|
||||
"triage_issue",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user