Skip to content

Commit a8a7196

Browse files
authored
Include PR review context in lens prompts (#79)
1 parent cca3cef commit a8a7196

2 files changed

Lines changed: 94 additions & 1 deletion

File tree

.github/scripts/evalops-pr-lens-review.rb

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ module EvalOpsPrLensReview
7777
DEFAULT_MODEL = "claude-opus-4-7"
7878
DEFAULT_MAX_DIFF_BYTES = 180_000
7979
MAX_FINDINGS_PER_COMMENT = 12
80+
MAX_CONTEXT_ITEMS = 25
8081

8182
module_function
8283

@@ -245,7 +246,60 @@ def pr_file_summary(repo:, pr:)
245246
end.join("\n")
246247
end
247248

248-
def build_lens_prompt(repo:, pr:, lens:, pr_json:, file_summary:, changed_files_text:, diff_text:, diff_truncated:)
249+
def short_text(value, max_bytes: 1_500)
250+
text = value.to_s.strip
251+
return "" if text.empty?
252+
return text if text.bytesize <= max_bytes
253+
254+
"#{text.byteslice(0, max_bytes)}\n...[truncated]"
255+
end
256+
257+
def list_section(title, rows)
258+
body = rows.compact.map(&:strip).reject(&:empty?)
259+
return "#{title}:\n(none)" if body.empty?
260+
261+
"#{title}:\n#{body.first(MAX_CONTEXT_ITEMS).join("\n")}"
262+
end
263+
264+
def pr_review_context(repo:, pr:, pr_json:, head_sha:)
265+
issue_comments = gh_api_json("repos/#{repo}/issues/#{pr}/comments?per_page=100")
266+
reviews = gh_api_json("repos/#{repo}/pulls/#{pr}/reviews?per_page=100")
267+
review_comments = gh_api_json("repos/#{repo}/pulls/#{pr}/comments?per_page=100")
268+
check_runs = gh_api_json("repos/#{repo}/commits/#{head_sha}/check-runs?per_page=100").fetch("check_runs", [])
269+
combined_status = gh_api_json("repos/#{repo}/commits/#{head_sha}/status")
270+
271+
comments = issue_comments.last(MAX_CONTEXT_ITEMS).map do |comment|
272+
"- #{comment.dig("user", "login")} at #{comment.fetch("created_at", "")}: #{short_text(comment["body"], max_bytes: 900)}"
273+
end
274+
review_rows = reviews.last(MAX_CONTEXT_ITEMS).map do |review|
275+
body = short_text(review["body"], max_bytes: 900)
276+
"- #{review.dig("user", "login")} #{review.fetch("state", "")} at #{review.fetch("submitted_at", "")}: #{body.empty? ? "(no body)" : body}"
277+
end
278+
inline_rows = review_comments.last(MAX_CONTEXT_ITEMS).map do |comment|
279+
line = comment["line"] || comment["original_line"] || "?"
280+
"- #{comment.dig("user", "login")} #{comment.fetch("path", "unknown")}:#{line}: #{short_text(comment["body"], max_bytes: 900)}"
281+
end
282+
check_rows = check_runs.select do |check|
283+
!%w[success skipped neutral].include?(check["conclusion"].to_s.downcase)
284+
end.map do |check|
285+
"- check-run #{check.fetch("name", "unknown")}: status=#{check.fetch("status", "")} conclusion=#{check["conclusion"] || "pending"}"
286+
end
287+
status_rows = Array(combined_status["statuses"]).select do |status|
288+
status["state"].to_s != "success"
289+
end.map do |status|
290+
"- status #{status.fetch("context", "unknown")}: state=#{status.fetch("state", "")} description=#{status["description"]}"
291+
end
292+
293+
[
294+
"Pull request body:\n#{short_text(pr_json["body"], max_bytes: 2_500).empty? ? "(none)" : short_text(pr_json["body"], max_bytes: 2_500)}",
295+
list_section("Issue comments", comments),
296+
list_section("PR review bodies", review_rows),
297+
list_section("Inline review comments", inline_rows),
298+
list_section("Non-green checks and statuses", check_rows + status_rows)
299+
].join("\n\n")
300+
end
301+
302+
def build_lens_prompt(repo:, pr:, lens:, pr_json:, file_summary:, review_context:, changed_files_text:, diff_text:, diff_truncated:)
249303
lens_config = LENSES.fetch(valid_lens!(lens))
250304
<<~PROMPT
251305
You are reviewing an EvalOps pull request through one narrow lens: #{lens_config.fetch(:name)}.
@@ -265,6 +319,8 @@ def build_lens_prompt(repo:, pr:, lens:, pr_json:, file_summary:, changed_files_
265319
- Report only actionable defects introduced by this PR that fit the lens.
266320
- Prefer no finding over a speculative finding.
267321
- Confidence must reflect direct evidence from the diff or live PR metadata.
322+
- Existing bot or human review comments are evidence, but verify them
323+
against the diff before turning them into a finding.
268324
- Use head-side file paths and line numbers where possible.
269325
- If no high-signal finding exists, return an empty findings array.
270326
- Do not ask for broad architecture redesigns, style-only changes, or unrelated cleanup.
@@ -292,6 +348,9 @@ def build_lens_prompt(repo:, pr:, lens:, pr_json:, file_summary:, changed_files_
292348
Pull request files from GitHub:
293349
#{file_summary.empty? ? "(no file metadata)" : file_summary}
294350
351+
Pull request context:
352+
#{review_context.empty? ? "(no PR context)" : review_context}
353+
295354
Changed files from git:
296355
#{changed_files_text.empty? ? "(no changed files)" : changed_files_text}
297356
@@ -455,6 +514,7 @@ def normalize_lens_review(raw_review, repo:, pr:, lens:, head_sha:)
455514
def run_lens(repo:, pr:, lens:, workspace:, base_sha:, head_sha:, output:, provider:, model:, max_diff_bytes:)
456515
pr_json = pr_metadata(repo: repo, pr: pr)
457516
file_summary = pr_file_summary(repo: repo, pr: pr)
517+
review_context = pr_review_context(repo: repo, pr: pr, pr_json: pr_json, head_sha: head_sha)
458518
changed_files_text = changed_files(workspace: workspace, base_sha: base_sha, head_sha: head_sha)
459519
diff_text, diff_truncated = git_diff(
460520
workspace: workspace,
@@ -468,6 +528,7 @@ def run_lens(repo:, pr:, lens:, workspace:, base_sha:, head_sha:, output:, provi
468528
lens: lens,
469529
pr_json: pr_json,
470530
file_summary: file_summary,
531+
review_context: review_context,
471532
changed_files_text: changed_files_text,
472533
diff_text: diff_text,
473534
diff_truncated: diff_truncated

test/evalops_pr_lens_review_test.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,38 @@ def is_a?(klass)
147147
refute_includes request_body.keys, "temperature"
148148
end
149149

150+
def test_build_lens_prompt_includes_review_context
151+
pr_json = {
152+
"title" => "Risky workflow",
153+
"html_url" => "https://github.com/evalops/deploy/pull/1",
154+
"draft" => false,
155+
"base" => {
156+
"ref" => "main",
157+
"sha" => "base"
158+
},
159+
"head" => {
160+
"ref" => "branch",
161+
"sha" => "head"
162+
}
163+
}
164+
165+
prompt = EvalOpsPrLensReview.build_lens_prompt(
166+
repo: "evalops/deploy",
167+
pr: 1,
168+
lens: "iam-blast-radius",
169+
pr_json: pr_json,
170+
file_summary: "modified\t.github/workflows/release.yml\t+10\t-2",
171+
review_context: "Inline review comments:\n- cursor .github/workflows/release.yml:42: token now has write-all",
172+
changed_files_text: "M\t.github/workflows/release.yml",
173+
diff_text: "@@ workflow diff @@",
174+
diff_truncated: false
175+
)
176+
177+
assert_includes prompt, "Pull request context:"
178+
assert_includes prompt, "token now has write-all"
179+
assert_includes prompt, "Existing bot or human review comments are evidence"
180+
end
181+
150182
private
151183

152184
def finding(title, confidence, priority, path, line)

0 commit comments

Comments
 (0)