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:
can1357
2026-06-02 08:23:09 +02:00
parent 04cc014c16
commit b503f7d86b
30 changed files with 2392 additions and 112 deletions
+115
View File
@@ -19,6 +19,7 @@ IssueState = Literal[
"new",
"reproducing",
"fixing",
"reviewing",
"opened",
"merged",
"closed",
@@ -72,6 +73,20 @@ CREATE TABLE IF NOT EXISTS tool_calls (
);
CREATE INDEX IF NOT EXISTS tool_calls_issue ON tool_calls(issue_key, ts);
CREATE TABLE IF NOT EXISTS pr_review_comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
issue_key TEXT NOT NULL,
path TEXT NOT NULL,
line INTEGER NOT NULL,
side TEXT NOT NULL DEFAULT 'RIGHT',
start_line INTEGER,
start_side TEXT,
body TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_pr_review_comments_key
ON pr_review_comments(issue_key);
CREATE TABLE IF NOT EXISTS submissions (
delivery_id TEXT PRIMARY KEY,
login TEXT NOT NULL,
@@ -132,6 +147,19 @@ class IssueRow:
classification: str | None = None
@dataclass(slots=True, frozen=True)
class StagedReviewComment:
id: int
issue_key: str
path: str
line: int
side: str
body: str
created_at: str
start_line: int | None = None
start_side: str | None = None
def _event_row_from_db_row(row: sqlite3.Row) -> EventRow:
return EventRow(
delivery_id=row["delivery_id"],
@@ -778,6 +806,93 @@ class Database:
)
return int(cur.lastrowid or 0)
def has_successful_tool_call(self, issue_key: str, tool: str) -> bool:
with self._lock:
row = self._conn.execute(
"""
SELECT 1
FROM tool_calls
WHERE issue_key=? AND tool=? AND error IS NULL
ORDER BY id DESC
LIMIT 1
""",
(issue_key, tool),
).fetchone()
return row is not None
# ---- PR review comment staging ----
def stage_review_comment(
self,
*,
issue_key: str,
path: str,
line: int,
body: str,
side: str = "RIGHT",
start_line: int | None = None,
start_side: str | None = None,
) -> StagedReviewComment:
with self._lock:
cur = self._conn.execute(
"""
INSERT INTO pr_review_comments
(issue_key, path, line, side, start_line, start_side, body, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(issue_key, path, line, side, start_line, start_side, body, _utcnow()),
)
row = self._conn.execute(
"""
SELECT id, issue_key, path, line, side, start_line, start_side, body, created_at
FROM pr_review_comments
WHERE id=?
""",
(int(cur.lastrowid or 0),),
).fetchone()
assert row is not None
return StagedReviewComment(
id=int(row["id"]),
issue_key=row["issue_key"],
path=row["path"],
line=int(row["line"]),
side=row["side"],
body=row["body"],
created_at=row["created_at"],
start_line=int(row["start_line"]) if row["start_line"] is not None else None,
start_side=row["start_side"],
)
def list_staged_review_comments(self, issue_key: str) -> list[StagedReviewComment]:
with self._lock:
rows = self._conn.execute(
"""
SELECT id, issue_key, path, line, side, start_line, start_side, body, created_at
FROM pr_review_comments
WHERE issue_key=?
ORDER BY id
""",
(issue_key,),
).fetchall()
return [
StagedReviewComment(
id=int(row["id"]),
issue_key=row["issue_key"],
path=row["path"],
line=int(row["line"]),
side=row["side"],
body=row["body"],
created_at=row["created_at"],
start_line=int(row["start_line"]) if row["start_line"] is not None else None,
start_side=row["start_side"],
)
for row in rows
]
def clear_staged_review_comments(self, issue_key: str) -> int:
with self._lock:
cur = self._conn.execute("DELETE FROM pr_review_comments WHERE issue_key=?", (issue_key,))
return int(cur.rowcount or 0)
# ---- submissions (per-user rate limiting) ----
def admit_submission(
self,