Skip to content

Commit eaa21c2

Browse files
committed
refactor: harden reviewer handoff command surface
1 parent b08bcde commit eaa21c2

20 files changed

Lines changed: 619 additions & 61 deletions

scripts/reviewer_bot_core/comment_command_policy.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
class OrdinaryCommandId(StrEnum):
1212
PASS = "pass"
1313
AWAY = "away"
14+
FEEDBACK = "feedback"
1415
DONE = "done"
1516
LABEL = "label"
1617
SYNC_MEMBERS = "sync-members"
@@ -103,6 +104,20 @@ def decide_comment_command(bot, request, classified, *, actor_class: str, comman
103104
success=False,
104105
react=True,
105106
)
107+
if command == OrdinaryCommandId.FEEDBACK.value:
108+
if args:
109+
return InlineResponseDecision(
110+
response=f"❌ `/feedback` does not accept arguments. Usage: `{bot.BOT_MENTION} /feedback`",
111+
success=False,
112+
react=True,
113+
)
114+
return ExecuteOrdinaryCommandDecision(
115+
command_id=OrdinaryCommandId.FEEDBACK,
116+
issue_number=issue_number,
117+
actor=comment_author,
118+
raw_args=(),
119+
needs_assignment_request=False,
120+
)
106121
if command == OrdinaryCommandId.DONE.value:
107122
return ExecuteOrdinaryCommandDecision(
108123
command_id=OrdinaryCommandId.DONE,

scripts/reviewer_bot_core/review_state_machine.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ def _reset_cycle_state(review_data: dict) -> None:
161161
review_data[channel] = {"accepted": None, "seen_keys": []}
162162
review_data["current_cycle_completion"] = {}
163163
review_data["current_cycle_write_approval"] = {}
164+
review_data["current_cycle_reviewer_handoff"] = None
164165
review_data["overdue_anchor"] = None
165166

166167

@@ -204,6 +205,9 @@ def clear_current_reviewer(state: dict, issue_number: int) -> bool:
204205
if review_data.get("assignment_method") is not None:
205206
review_data["assignment_method"] = None
206207
changed = True
208+
if review_data.get("current_cycle_reviewer_handoff") is not None:
209+
review_data["current_cycle_reviewer_handoff"] = None
210+
changed = True
207211
clear_transition_timers(review_data)
208212
return changed
209213

scripts/reviewer_bot_core/review_state_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,4 @@ class ReviewEntryState:
7979
review_dismissal: ReviewChannelState = field(default_factory=ReviewChannelState)
8080
current_cycle_completion: dict[str, Any] = field(default_factory=dict)
8181
current_cycle_write_approval: dict[str, Any] = field(default_factory=dict)
82+
current_cycle_reviewer_handoff: dict[str, Any] | None = None

scripts/reviewer_bot_core/reviewer_response_policy.py

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,43 @@ def _record_for_current_reviewer(record: dict | None | object, current_reviewer:
195195
return record
196196

197197

198+
def _current_cycle_reviewer_handoff_record(
199+
review_data: dict,
200+
current_reviewer: str,
201+
current_head: str | None,
202+
*,
203+
issue_is_pull_request: bool,
204+
) -> dict | None:
205+
handoff = review_data.get("current_cycle_reviewer_handoff")
206+
if not isinstance(handoff, dict):
207+
return None
208+
if handoff.get("command_name") != "feedback":
209+
return None
210+
actor = handoff.get("actor")
211+
timestamp = handoff.get("timestamp")
212+
source_event_key = handoff.get("source_event_key")
213+
if not isinstance(actor, str) or actor.lower() != current_reviewer.lower():
214+
return None
215+
if not isinstance(timestamp, str) or not timestamp.strip():
216+
return None
217+
if not isinstance(source_event_key, str) or not source_event_key.strip():
218+
return None
219+
reviewed_head_sha = handoff.get("reviewed_head_sha")
220+
if issue_is_pull_request:
221+
if not isinstance(reviewed_head_sha, str) or reviewed_head_sha != current_head:
222+
return None
223+
elif reviewed_head_sha is not None:
224+
return None
225+
return {
226+
"semantic_key": source_event_key,
227+
"timestamp": timestamp,
228+
"actor": actor,
229+
"reviewed_head_sha": reviewed_head_sha,
230+
"source_precedence": 1,
231+
"payload": {"command_name": "feedback"},
232+
}
233+
234+
198235
def _contributor_revision_handoff_record(review_data: dict, current_head: str | None, reviewer_review: dict | None) -> dict | None:
199236
contributor_revision = review_data.get("contributor_revision", {}).get("accepted")
200237
if not isinstance(contributor_revision, dict):
@@ -283,6 +320,18 @@ def derive_reviewer_response_state(
283320
parse_timestamp=live_review_support.parse_github_timestamp,
284321
) > 0:
285322
latest_reviewer_response = reviewer_review
323+
reviewer_handoff = _current_cycle_reviewer_handoff_record(
324+
review_data,
325+
current_reviewer,
326+
None,
327+
issue_is_pull_request=False,
328+
)
329+
if reviewer_review_helpers.compare_records(
330+
reviewer_handoff,
331+
latest_reviewer_response,
332+
parse_timestamp=live_review_support.parse_github_timestamp,
333+
) > 0:
334+
latest_reviewer_response = reviewer_handoff
286335
completion = review_data.get("current_cycle_completion")
287336
if isinstance(completion, dict) and completion.get("completed"):
288337
return _decorate_response(
@@ -292,6 +341,7 @@ def derive_reviewer_response_state(
292341
anchor_timestamp=latest_reviewer_response.get("timestamp") if isinstance(latest_reviewer_response, dict) else None,
293342
reviewer_comment=reviewer_comment,
294343
reviewer_review=reviewer_review,
344+
current_cycle_reviewer_handoff=reviewer_handoff,
295345
contributor_comment=contributor_comment,
296346
contributor_handoff=contributor_comment,
297347
)
@@ -303,6 +353,7 @@ def derive_reviewer_response_state(
303353
anchor_timestamp=latest_reviewer_response.get("timestamp") if isinstance(latest_reviewer_response, dict) else None,
304354
reviewer_comment=reviewer_comment,
305355
reviewer_review=reviewer_review,
356+
current_cycle_reviewer_handoff=reviewer_handoff,
306357
contributor_comment=contributor_comment,
307358
contributor_handoff=contributor_comment,
308359
)
@@ -314,6 +365,7 @@ def derive_reviewer_response_state(
314365
anchor_timestamp=_initial_reviewer_anchor(review_data),
315366
reviewer_comment=reviewer_comment,
316367
reviewer_review=reviewer_review,
368+
current_cycle_reviewer_handoff=reviewer_handoff,
317369
contributor_comment=contributor_comment,
318370
contributor_handoff=contributor_comment,
319371
)
@@ -329,6 +381,7 @@ def derive_reviewer_response_state(
329381
anchor_timestamp=contributor_comment.get("timestamp") if isinstance(contributor_comment, dict) else None,
330382
reviewer_comment=reviewer_comment,
331383
reviewer_review=reviewer_review,
384+
current_cycle_reviewer_handoff=reviewer_handoff,
332385
contributor_comment=contributor_comment,
333386
contributor_handoff=contributor_comment,
334387
)
@@ -339,6 +392,7 @@ def derive_reviewer_response_state(
339392
anchor_timestamp=latest_reviewer_response.get("timestamp") if isinstance(latest_reviewer_response, dict) else None,
340393
reviewer_comment=reviewer_comment,
341394
reviewer_review=reviewer_review,
395+
current_cycle_reviewer_handoff=reviewer_handoff,
342396
contributor_comment=contributor_comment,
343397
contributor_handoff=contributor_comment,
344398
)
@@ -350,7 +404,14 @@ def derive_reviewer_response_state(
350404
scope_fields={"current_scope_key": None, "current_scope_basis": None},
351405
)
352406

353-
if not reviewer_comment and not reviewer_review:
407+
reviewer_handoff = _current_cycle_reviewer_handoff_record(
408+
review_data,
409+
current_reviewer,
410+
current_head,
411+
issue_is_pull_request=True,
412+
)
413+
414+
if not reviewer_comment and not reviewer_review and not reviewer_handoff:
354415
if not had_reviewer_review:
355416
return _decorate_response(
356417
state="awaiting_reviewer_response",
@@ -359,6 +420,7 @@ def derive_reviewer_response_state(
359420
anchor_timestamp=_initial_reviewer_anchor(review_data),
360421
reviewer_comment=reviewer_comment,
361422
reviewer_review=reviewer_review,
423+
current_cycle_reviewer_handoff=reviewer_handoff,
362424
contributor_comment=contributor_comment,
363425
contributor_handoff=None,
364426
)
@@ -370,6 +432,12 @@ def derive_reviewer_response_state(
370432
parse_timestamp=live_review_support.parse_github_timestamp,
371433
) > 0:
372434
latest_reviewer_response = reviewer_review
435+
if reviewer_review_helpers.compare_records(
436+
reviewer_handoff,
437+
latest_reviewer_response,
438+
parse_timestamp=live_review_support.parse_github_timestamp,
439+
) > 0:
440+
latest_reviewer_response = reviewer_handoff
373441

374442
contributor_handoff = contributor_comment
375443
contributor_revision = _contributor_revision_handoff_record(
@@ -402,6 +470,7 @@ def derive_reviewer_response_state(
402470
current_head_sha=current_head,
403471
reviewer_comment=reviewer_comment,
404472
reviewer_review=reviewer_review,
473+
current_cycle_reviewer_handoff=reviewer_handoff,
405474
contributor_comment=contributor_comment,
406475
contributor_handoff=contributor_handoff,
407476
)
@@ -419,6 +488,7 @@ def derive_reviewer_response_state(
419488
current_head_sha=current_head,
420489
reviewer_comment=reviewer_comment,
421490
reviewer_review=reviewer_review,
491+
current_cycle_reviewer_handoff=reviewer_handoff,
422492
contributor_comment=contributor_comment,
423493
contributor_handoff=contributor_handoff,
424494
)
@@ -438,21 +508,24 @@ def derive_reviewer_response_state(
438508
current_head_sha=current_head,
439509
reviewer_comment=reviewer_comment,
440510
reviewer_review=reviewer_review,
511+
current_cycle_reviewer_handoff=reviewer_handoff,
441512
contributor_comment=contributor_comment,
442513
contributor_handoff=contributor_handoff,
443514
)
444515

445516
latest_review_head = reviewer_review.get("reviewed_head_sha") if isinstance(reviewer_review, dict) else None
446517
if not isinstance(latest_review_head, str) or latest_review_head != current_head:
447-
if isinstance(reviewer_comment, dict):
518+
if isinstance(reviewer_comment, dict) or isinstance(reviewer_handoff, dict):
519+
response_anchor = reviewer_comment if isinstance(reviewer_comment, dict) else reviewer_handoff
448520
return _decorate_response(
449521
state="awaiting_contributor_response",
450522
reason="accepted_same_scope_reviewer_activity",
451523
scope_fields=_current_scope_fields(review_data, current_reviewer, current_head, contributor_handoff),
452-
anchor_timestamp=reviewer_comment.get("timestamp") if isinstance(reviewer_comment.get("timestamp"), str) else None,
524+
anchor_timestamp=response_anchor.get("timestamp") if isinstance(response_anchor.get("timestamp"), str) else None,
453525
current_head_sha=current_head,
454526
reviewer_comment=reviewer_comment,
455527
reviewer_review=reviewer_review,
528+
current_cycle_reviewer_handoff=reviewer_handoff,
456529
contributor_comment=contributor_comment,
457530
contributor_handoff=contributor_handoff,
458531
)
@@ -464,6 +537,7 @@ def derive_reviewer_response_state(
464537
current_head_sha=current_head,
465538
reviewer_comment=reviewer_comment,
466539
reviewer_review=reviewer_review,
540+
current_cycle_reviewer_handoff=reviewer_handoff,
467541
contributor_comment=contributor_comment,
468542
contributor_handoff=contributor_handoff,
469543
)
@@ -486,6 +560,7 @@ def derive_reviewer_response_state(
486560
current_head_sha=current_head,
487561
reviewer_comment=reviewer_comment,
488562
reviewer_review=reviewer_review,
563+
current_cycle_reviewer_handoff=reviewer_handoff,
489564
contributor_comment=contributor_comment,
490565
contributor_handoff=contributor_handoff,
491566
)
@@ -498,6 +573,7 @@ def derive_reviewer_response_state(
498573
current_head_sha=current_head,
499574
reviewer_comment=reviewer_comment,
500575
reviewer_review=reviewer_review,
576+
current_cycle_reviewer_handoff=reviewer_handoff,
501577
contributor_comment=contributor_comment,
502578
contributor_handoff=contributor_handoff,
503579
)
@@ -509,6 +585,7 @@ def derive_reviewer_response_state(
509585
current_head_sha=current_head,
510586
reviewer_comment=reviewer_comment,
511587
reviewer_review=reviewer_review,
588+
current_cycle_reviewer_handoff=reviewer_handoff,
512589
contributor_comment=contributor_comment,
513590
contributor_handoff=contributor_handoff,
514591
)

scripts/reviewer_bot_core/state_adapters.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,9 @@ def review_entry_from_persisted(review_entry: dict[str, Any] | list[Any] | None)
297297
current_cycle_write_approval=deepcopy(review_entry.get("current_cycle_write_approval") or {})
298298
if isinstance(review_entry.get("current_cycle_write_approval"), dict)
299299
else {},
300+
current_cycle_reviewer_handoff=deepcopy(review_entry.get("current_cycle_reviewer_handoff"))
301+
if isinstance(review_entry.get("current_cycle_reviewer_handoff"), dict)
302+
else None,
300303
)
301304

302305

@@ -328,6 +331,7 @@ def review_entry_to_persisted(review_entry: ReviewEntryState) -> dict[str, Any]:
328331
"review_dismissal": _channel_to_persisted(review_entry.review_dismissal),
329332
"current_cycle_completion": deepcopy(review_entry.current_cycle_completion),
330333
"current_cycle_write_approval": deepcopy(review_entry.current_cycle_write_approval),
334+
"current_cycle_reviewer_handoff": deepcopy(review_entry.current_cycle_reviewer_handoff),
331335
}
332336

333337

scripts/reviewer_bot_lib/assignment_flow.py

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -117,14 +117,6 @@ def resolve_reviewer_authority(
117117
is_pull_request: bool,
118118
) -> dict[str, object]:
119119
tracked_reviewer = review_data.get("current_reviewer") if isinstance(review_data, dict) else None
120-
if not isinstance(tracked_reviewer, str) or not tracked_reviewer.strip():
121-
return {
122-
"authority_status": "no_tracked_reviewer",
123-
"tracked_reviewer": None,
124-
"live_control_plane_reviewers": [],
125-
"reason": "no_tracked_reviewer",
126-
}
127-
128120
result = bot.github.get_issue_assignees_result(issue_number, is_pull_request=is_pull_request)
129121
_hard_fail_if_permission_denied(result, action="reviewer authority read", issue_number=issue_number)
130122
if not result.ok or not isinstance(result.payload, list):
@@ -136,6 +128,13 @@ def resolve_reviewer_authority(
136128
}
137129

138130
live_control_plane_reviewers = [value for value in result.payload if isinstance(value, str) and value.strip()]
131+
if not isinstance(tracked_reviewer, str) or not tracked_reviewer.strip():
132+
return {
133+
"authority_status": "no_tracked_reviewer",
134+
"tracked_reviewer": None,
135+
"live_control_plane_reviewers": live_control_plane_reviewers,
136+
"reason": "no_tracked_reviewer",
137+
}
139138
normalized_reviewers = {value.lower() for value in live_control_plane_reviewers}
140139
tracked_key = tracked_reviewer.lower()
141140
if is_pull_request:
@@ -175,6 +174,80 @@ def resolve_reviewer_authority(
175174
}
176175

177176

177+
def resolve_reviewer_command_authority(
178+
bot,
179+
state: dict,
180+
request,
181+
*,
182+
actor: str | None = None,
183+
) -> dict[str, object]:
184+
issue_number = request.issue_number
185+
review_data = ensure_review_entry(state, issue_number)
186+
if review_data is None:
187+
result = bot.github.get_issue_assignees_result(issue_number, is_pull_request=bool(request.is_pull_request))
188+
_hard_fail_if_permission_denied(result, action="reviewer authority read", issue_number=issue_number)
189+
if not result.ok or not isinstance(result.payload, list):
190+
return {
191+
"authorized": False,
192+
"authorization_status": "live_read_unavailable",
193+
"review_data": None,
194+
"tracked_reviewer": None,
195+
"live_control_plane_reviewers": [],
196+
"reason": str(result.failure_kind or "live_control_plane_unavailable"),
197+
}
198+
return {
199+
"authorized": False,
200+
"authorization_status": "no_active_review",
201+
"review_data": None,
202+
"tracked_reviewer": None,
203+
"live_control_plane_reviewers": [value for value in result.payload if isinstance(value, str) and value.strip()],
204+
"reason": "no_active_review",
205+
}
206+
authority = resolve_reviewer_authority(
207+
bot,
208+
issue_number,
209+
review_data,
210+
is_pull_request=bool(request.is_pull_request),
211+
)
212+
status = str(authority.get("authority_status"))
213+
resolution = {
214+
"authorized": status == "tracked_reviewer_confirmed",
215+
"authorization_status": status,
216+
"review_data": review_data,
217+
"tracked_reviewer": authority.get("tracked_reviewer"),
218+
"live_control_plane_reviewers": list(authority.get("live_control_plane_reviewers") or []),
219+
"reason": authority.get("reason"),
220+
}
221+
if not resolution["authorized"] or actor is None:
222+
return resolution
223+
tracked_reviewer = resolution.get("tracked_reviewer")
224+
if not isinstance(tracked_reviewer, str) or tracked_reviewer.lower() != actor.lower():
225+
resolution["authorized"] = False
226+
resolution["authorization_status"] = "actor_not_current_reviewer"
227+
resolution["reason"] = "actor_not_current_reviewer"
228+
return resolution
229+
230+
231+
def reviewer_command_authority_failure_message(command_name: str, resolution: dict[str, object]) -> str:
232+
status = str(resolution.get("authorization_status") or "")
233+
tracked_reviewer = resolution.get("tracked_reviewer")
234+
live_reviewers = [value for value in resolution.get("live_control_plane_reviewers") or [] if isinstance(value, str)]
235+
if status == "live_read_unavailable":
236+
return "❌ Unable to determine current assignees/reviewers from GitHub; refusing to continue."
237+
if status in {"no_active_review", "no_tracked_reviewer"}:
238+
return "❌ No active tracked review exists for this issue/PR."
239+
if status == "actor_not_current_reviewer" and isinstance(tracked_reviewer, str) and tracked_reviewer:
240+
return f"❌ Only the current reviewer (@{tracked_reviewer}) can use `/{command_name}`."
241+
if status == "control_plane_mismatch":
242+
if live_reviewers:
243+
return (
244+
f"❌ Unable to confirm @{tracked_reviewer} as the current reviewer from GitHub. "
245+
f"Live reviewer(s): @{', @'.join(live_reviewers)}."
246+
)
247+
return f"❌ Unable to confirm @{tracked_reviewer} as the current reviewer from GitHub."
248+
return f"❌ Unable to confirm current reviewer authority for `/{command_name}`."
249+
250+
178251
def _post_assignment_guidance(bot, request, reviewer: str) -> None:
179252
if request.is_pull_request:
180253
bot.github.post_comment(request.issue_number, get_pr_guidance(reviewer, request.issue_author))

0 commit comments

Comments
 (0)