Enforce single terminal review decision per PR review run (Issue #211)

Reviewer agents could post probe APPROVE/REQUEST_CHANGES reviews while
testing lock paths, polluting PR audit trails. Add a review decision lock
seeded by gitea_resolve_task_capability(review_pr) that requires
gitea_mark_final_review_decision and final_review_decision_ready=True
before gitea_submit_pr_review performs a live mutation.

Add gitea_dry_run_pr_review for read-only submission validation,
gitea_authorize_review_correction for operator-approved fixes, and
assess_review_mutation_final_report for final-report proof. One live
review mutation per run unless correction is explicitly authorized.
This commit is contained in:
2026-07-05 17:43:36 -04:00
parent 5b9fcaefbf
commit 880fca81da
5 changed files with 556 additions and 71 deletions
+48
View File
@@ -582,6 +582,54 @@ def assess_role_boundary(proof=None, *, task_role=None, namespaces_used=None,
}
def assess_review_mutation_final_report(report_text, review_decision_lock):
"""Require final reports to list exactly one live review mutation.
Two mutations are allowed only when an operator-approved correction flow
was invoked and explained in the report.
"""
lock = review_decision_lock or {}
text = report_text or ""
lower = text.lower()
mutations = list(lock.get("live_mutations") or [])
missing = []
count = len(mutations)
if count == 0:
if any(term in lower for term in ("submitted 'approve'", "submitted 'request_changes'", "live review mutation")):
missing.append("review mutation count")
elif count == 1:
m = mutations[0]
action = m.get("action")
pr_number = m.get("pr_number")
if action and action.lower() not in lower:
missing.append("review mutation action")
if pr_number is not None and f"#{pr_number}" not in lower and f"pr {pr_number}" not in lower:
missing.append("review mutation PR number")
elif count > 1:
if not lock.get("correction_authorized") and "correction" not in lower:
missing.append("correction flow explanation")
if "review mutations:" not in lower and "review mutation" not in lower:
missing.append("review mutation listing")
if missing:
return {
"complete": False,
"downgraded": True,
"missing_fields": missing,
"reasons": [
f"final report missing review-mutation field: {field}"
for field in missing
],
}
return {
"complete": True,
"downgraded": False,
"missing_fields": [],
"reasons": [],
}
def build_final_report(checkout_proof, inventory, validation, contamination,
identity_eligible, merge_performed,
issue_status_verified,