fix: resolve reviewer preflight capability/lease deadlock and add gitea_release_reviewer_pr_lease tool (closes #546)

This commit is contained in:
2026-07-08 15:29:18 -04:00
parent eeadb1bbea
commit dfbee45309
2 changed files with 287 additions and 1 deletions
+122 -1
View File
@@ -2409,10 +2409,18 @@ def _review_decision_session_reasons(lock: dict | None) -> list[str]:
return reasons
def init_review_decision_lock(remote: str | None, task: str | None):
def init_review_decision_lock(remote: str | None, task: str | None, force: bool = True):
"""Seed read-only-until-ready state for reviewer PR review tasks."""
if task != "review_pr":
return
if not force:
lock = _load_review_decision_lock()
if lock is not None:
if lock.get("remote") == remote and lock.get("session_pid") == os.getpid():
env_lock = (os.environ.get(SESSION_PROFILE_LOCK_ENV) or "").strip()
stored_lock = (lock.get("session_profile_lock") or "").strip()
if not env_lock or not stored_lock or env_lock == stored_lock:
return
review_workflow_load.clear_review_workflow_load()
profile = get_profile()
profile_name = (profile.get("profile_name") or "").strip()
@@ -5939,6 +5947,118 @@ def gitea_cleanup_post_merge_moot_lease(
return report
@mcp.tool()
def gitea_release_reviewer_pr_lease(
pr_number: int,
worktree: str | None = None,
remote: str = "dadeschools",
host: str | None = None,
org: str | None = None,
repo: str | None = None,
) -> dict:
"""Safely release an active reviewer lease owned by the current session on an open PR.
This provides a canonical way to clean up reviewer leases when a review fails
before submission.
Args:
pr_number: The PR number whose reviewer lease to release.
worktree: Optional worktree path (resolves automatically if not supplied).
remote: Known instance 'dadeschools' or 'prgs'.
host: Override the Gitea host.
org: Override the owner/organization.
repo: Override the repository name.
Returns:
dict reporting release status.
"""
_verify_role_mutation_workspace(
remote, worktree=worktree, task="review_pr"
)
h, o, r = _resolve(remote, host, org, repo)
auth = _auth(h)
comments = _fetch_pr_comments(
pr_number, remote=remote, host=host, org=org, repo=repo
)
active = reviewer_pr_lease.find_active_reviewer_lease(
comments, pr_number=pr_number
)
if not active:
return {
"success": True,
"released": False,
"reasons": ["no active reviewer lease found on PR"],
}
identity = _authenticated_username(remote) or ""
session = reviewer_pr_lease.get_session_lease() or {}
session_id = session.get("session_id")
owner_identity = active.get("reviewer_identity")
owner_session_id = active.get("session_id")
# Authorize release: identity matches, session_id matches, or lease is expired/reclaimable
authorized = False
reasons = []
if owner_identity and identity and owner_identity == identity:
authorized = True
elif owner_session_id and session_id and owner_session_id == session_id:
authorized = True
else:
freshness = reviewer_pr_lease.classify_lease_freshness(active)
if freshness in {"expired", "reclaimable"}:
authorized = True
if not authorized:
return {
"success": False,
"released": False,
"reasons": [
f"unauthorized to release active lease: owned by {owner_identity} "
f"(session {owner_session_id})"
],
}
# Format and post release comment
body = reviewer_pr_lease.format_lease_body(
repo=active.get("repo") or f"{o}/{r}",
pr_number=pr_number,
issue_number=active.get("issue_number"),
reviewer_identity=owner_identity or identity,
profile=active.get("profile") or "reviewer",
session_id=owner_session_id or session_id or "unknown",
worktree=active.get("worktree") or "",
phase="released",
candidate_head=active.get("candidate_head"),
target_branch=active.get("target_branch") or "master",
target_branch_sha=active.get("target_branch_sha"),
blocker="manual-release",
)
comment_url = f"{repo_api_url(h, o, r)}/issues/{pr_number}/comments"
with _audited(
"comment_pr",
host=h,
remote=remote,
org=o,
repo=r,
pr_number=pr_number,
request_metadata={"source": "release_reviewer_pr_lease"},
):
posted = api_request("POST", comment_url, auth, {"body": body})
reviewer_pr_lease.clear_session_lease()
return {
"success": True,
"released": True,
"comment_id": posted.get("id"),
"reasons": [],
}
@mcp.tool()
def gitea_list_issue_comments(
issue_number: int,
@@ -8491,6 +8611,7 @@ def gitea_resolve_task_capability(
init_review_decision_lock(
remote if remote in REMOTES else None,
task,
force=False,
)
record_mutation_authority(profile["profile_name"], username, remote if remote in REMOTES else None, task)