feat(lease): guarded cleanup for obsolete reviewer comment leases (Closes #691)
Add fail-closed assessment and MCP tool for expired/superseded-head comment-backed reviewer leases that lack a control-plane lease_id. - assess_obsolete_reviewer_comment_lease_cleanup eligibility matrix - diagnose classifications: foreign_active_current_head, foreign_expired_current_head, foreign_completed_superseded_head, foreign_expired_superseded_head, orphaned_owner_missing, ambiguous_conflicting_evidence - gitea_cleanup_obsolete_reviewer_comment_lease with confirmation CLEANUP OBSOLETE REVIEWER LEASE <n> and controller_recovery_authorized - preserve audit history via append-only phase=released marker - never repoint, transfer validation, use PID ownership, or steal active current-head foreign leases - regression for PR #688 lease comment 10749 after recorded expiry
This commit is contained in:
+275
-5
@@ -7964,11 +7964,12 @@ def gitea_diagnose_reviewer_pr_lease_handoff(
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
) -> dict:
|
||||
"""Read-only: diagnose open-PR reviewer lease handoff and emit next action (#599).
|
||||
"""Read-only: diagnose open-PR reviewer lease handoff and emit next action (#599, #691).
|
||||
|
||||
Classifies no-lease / own / foreign / instructed-lease-missing-with-replacement
|
||||
and worktree binding mismatch. Returns a canonical ``next_action`` without
|
||||
mutating Gitea state. Never steals foreign leases.
|
||||
Classifies no-lease / own / foreign / superseded-head / expired / orphan /
|
||||
instructed-lease-missing-with-replacement and worktree binding mismatch.
|
||||
Returns a canonical ``next_action`` (including guarded obsolete-lease cleanup)
|
||||
without mutating Gitea state. Never steals foreign leases.
|
||||
"""
|
||||
read_block = _profile_operation_gate("gitea.read")
|
||||
if read_block:
|
||||
@@ -7979,7 +7980,7 @@ def gitea_diagnose_reviewer_pr_lease_handoff(
|
||||
}
|
||||
comments = _fetch_pr_comments(
|
||||
pr_number, remote=remote, host=host, org=org, repo=repo)
|
||||
h, _o, _r = _resolve(remote, host, org, repo)
|
||||
h, o, r = _resolve(remote, host, org, repo)
|
||||
try:
|
||||
username = _authenticated_username(h)
|
||||
except Exception:
|
||||
@@ -7993,6 +7994,64 @@ def gitea_diagnose_reviewer_pr_lease_handoff(
|
||||
# Prefer in-session lease id when present; otherwise diagnose as a fresh
|
||||
# caller without inventing ownership of an on-thread lease.
|
||||
session_id = (session_lease.get("session_id") or "").strip() or None
|
||||
|
||||
# Live PR head + formal reviews for #691 superseded/completed classification.
|
||||
current_head_sha = None
|
||||
formal_reviews: list[dict] = []
|
||||
try:
|
||||
auth = _auth(h)
|
||||
pr_live = api_request(
|
||||
"GET", f"{repo_api_url(h, o, r)}/pulls/{pr_number}", auth
|
||||
) or {}
|
||||
head = pr_live.get("head") or {}
|
||||
current_head_sha = head.get("sha") or pr_live.get("head_commit_sha")
|
||||
try:
|
||||
reviews = api_request(
|
||||
"GET",
|
||||
f"{repo_api_url(h, o, r)}/pulls/{pr_number}/reviews",
|
||||
auth,
|
||||
) or []
|
||||
for rev in reviews if isinstance(reviews, list) else []:
|
||||
formal_reviews.append({
|
||||
"verdict": rev.get("state") or rev.get("verdict"),
|
||||
"reviewed_head_sha": rev.get("commit_id") or rev.get(
|
||||
"reviewed_head_sha"
|
||||
),
|
||||
"dismissed": bool(rev.get("dismissed")),
|
||||
"reviewer": ((rev.get("user") or {}).get("login")),
|
||||
"submitted_at": rev.get("submitted_at"),
|
||||
})
|
||||
except Exception:
|
||||
formal_reviews = []
|
||||
except Exception:
|
||||
current_head_sha = None
|
||||
|
||||
lease_wt = None
|
||||
active = reviewer_pr_lease.find_newest_nonterminal_lease(
|
||||
comments, pr_number=pr_number, include_expired=True
|
||||
)
|
||||
if active:
|
||||
lease_wt = (active.get("worktree") or "").strip() or None
|
||||
worktree_exists = None
|
||||
worktree_clean = None
|
||||
if lease_wt:
|
||||
worktree_exists = os.path.isdir(lease_wt)
|
||||
if worktree_exists:
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
st = subprocess.run(
|
||||
["git", "-C", lease_wt, "status", "--porcelain"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
check=False,
|
||||
)
|
||||
if st.returncode == 0:
|
||||
worktree_clean = not bool((st.stdout or "").strip())
|
||||
except Exception:
|
||||
worktree_clean = None
|
||||
|
||||
diagnosis = reviewer_pr_lease.diagnose_reviewer_pr_lease_handoff(
|
||||
comments,
|
||||
pr_number=pr_number,
|
||||
@@ -8002,6 +8061,11 @@ def gitea_diagnose_reviewer_pr_lease_handoff(
|
||||
env_bound_worktree=env_wt,
|
||||
instructed_session_id=instructed_session_id,
|
||||
instructed_comment_id=instructed_comment_id,
|
||||
current_head_sha=current_head_sha,
|
||||
formal_reviews=formal_reviews,
|
||||
worktree_exists=worktree_exists,
|
||||
worktree_clean=worktree_clean,
|
||||
owner_process_alive=None, # PID never proves ownership; leave unset
|
||||
)
|
||||
diagnosis["success"] = True
|
||||
diagnosis["remote"] = remote
|
||||
@@ -8129,6 +8193,212 @@ def gitea_cleanup_post_merge_moot_lease(
|
||||
return report
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_cleanup_obsolete_reviewer_comment_lease(
|
||||
pr_number: int,
|
||||
confirmation: str = "",
|
||||
apply: bool = False,
|
||||
controller_recovery_authorized: bool = False,
|
||||
expected_lease_comment_id: int | None = None,
|
||||
expected_session_id: str | None = None,
|
||||
expected_leased_head: str | None = None,
|
||||
owner_process_alive: bool | None = None,
|
||||
current_head_review_in_progress: bool = False,
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
) -> dict:
|
||||
"""Guarded cleanup for obsolete comment-backed reviewer leases on open PRs (#691).
|
||||
|
||||
Neutralises an expired and/or superseded-head foreign lease by posting a
|
||||
terminal ``phase: released`` lease marker (append-only audit). Never deletes
|
||||
comments, never repoints the lease to the new head, never transfers
|
||||
validation/decision/workflow state, never uses PID equality as ownership,
|
||||
and never steals a genuinely active foreign lease on the current head.
|
||||
|
||||
Read-first when ``apply`` is false. Apply requires:
|
||||
|
||||
- ``controller_recovery_authorized=True`` (explicit operator/controller
|
||||
recovery capability — not implied by profile alone)
|
||||
- ``confirmation`` exactly ``CLEANUP OBSOLETE REVIEWER LEASE <pr_number>``
|
||||
- full eligibility evidence from
|
||||
``reviewer_pr_lease.assess_obsolete_reviewer_comment_lease_cleanup``
|
||||
|
||||
Comment-backed leases need no control-plane DB ``lease_id``.
|
||||
"""
|
||||
read_block = _profile_operation_gate("gitea.read")
|
||||
if read_block:
|
||||
return {
|
||||
"success": False,
|
||||
"cleanup_performed": False,
|
||||
"no_steal_or_adoption": True,
|
||||
"reasons": read_block,
|
||||
"permission_report": _permission_block_report("gitea.read"),
|
||||
}
|
||||
|
||||
h, o, r = _resolve(remote, host, org, repo)
|
||||
auth = _auth(h)
|
||||
expected_repo = f"{o}/{r}"
|
||||
comments = _fetch_pr_comments(
|
||||
pr_number, remote=remote, host=host, org=org, repo=repo
|
||||
)
|
||||
pr_live = api_request(
|
||||
"GET", f"{repo_api_url(h, o, r)}/pulls/{pr_number}", auth
|
||||
) or {}
|
||||
head = pr_live.get("head") or {}
|
||||
current_head_sha = head.get("sha") or pr_live.get("head_commit_sha")
|
||||
|
||||
formal_reviews: list[dict] = []
|
||||
try:
|
||||
reviews = api_request(
|
||||
"GET",
|
||||
f"{repo_api_url(h, o, r)}/pulls/{pr_number}/reviews",
|
||||
auth,
|
||||
) or []
|
||||
for rev in reviews if isinstance(reviews, list) else []:
|
||||
formal_reviews.append({
|
||||
"verdict": rev.get("state") or rev.get("verdict"),
|
||||
"reviewed_head_sha": rev.get("commit_id")
|
||||
or rev.get("reviewed_head_sha"),
|
||||
"dismissed": bool(rev.get("dismissed")),
|
||||
"reviewer": ((rev.get("user") or {}).get("login")),
|
||||
"submitted_at": rev.get("submitted_at"),
|
||||
})
|
||||
except Exception:
|
||||
formal_reviews = []
|
||||
|
||||
lease = reviewer_pr_lease.find_newest_nonterminal_lease(
|
||||
comments, pr_number=pr_number, include_expired=True
|
||||
)
|
||||
lease_wt = ((lease or {}).get("worktree") or "").strip() or None
|
||||
worktree_exists = None
|
||||
worktree_clean = None
|
||||
worktree_has_unpreserved_work = None
|
||||
if lease_wt:
|
||||
worktree_exists = os.path.isdir(lease_wt)
|
||||
if worktree_exists:
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
st = subprocess.run(
|
||||
["git", "-C", lease_wt, "status", "--porcelain"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
check=False,
|
||||
)
|
||||
if st.returncode == 0:
|
||||
dirty = bool((st.stdout or "").strip())
|
||||
worktree_clean = not dirty
|
||||
worktree_has_unpreserved_work = dirty
|
||||
except Exception:
|
||||
worktree_clean = None
|
||||
|
||||
session = reviewer_pr_lease.get_session_lease() or {}
|
||||
assessment = reviewer_pr_lease.assess_obsolete_reviewer_comment_lease_cleanup(
|
||||
comments,
|
||||
pr_number=pr_number,
|
||||
current_head_sha=current_head_sha,
|
||||
formal_reviews=formal_reviews,
|
||||
repo=expected_repo,
|
||||
expected_repo=expected_repo,
|
||||
requesting_session_id=(session.get("session_id") or "").strip() or None,
|
||||
controller_recovery_authorized=controller_recovery_authorized,
|
||||
worktree_exists=worktree_exists,
|
||||
worktree_clean=worktree_clean,
|
||||
worktree_has_unpreserved_work=worktree_has_unpreserved_work,
|
||||
owner_process_alive=owner_process_alive,
|
||||
owner_pid_observed=None,
|
||||
requesting_pid=os.getpid(),
|
||||
current_head_review_in_progress=current_head_review_in_progress,
|
||||
expected_lease_comment_id=expected_lease_comment_id,
|
||||
expected_session_id=expected_session_id,
|
||||
expected_leased_head=expected_leased_head,
|
||||
confirmation=confirmation,
|
||||
apply=apply,
|
||||
)
|
||||
|
||||
report = {
|
||||
"success": True,
|
||||
"pr_number": pr_number,
|
||||
"pr_state": pr_live.get("state"),
|
||||
"cleanup_allowed": assessment.get("cleanup_allowed"),
|
||||
"cleanup_performed": False,
|
||||
"classification": assessment.get("classification"),
|
||||
"blocker_kind": assessment.get("blocker_kind"),
|
||||
"exact_next_action": assessment.get("exact_next_action"),
|
||||
"mutation_eligibility": assessment.get("mutation_eligibility"),
|
||||
"leased_head": assessment.get("leased_head"),
|
||||
"current_head": assessment.get("current_head"),
|
||||
"expires_at": assessment.get("expires_at"),
|
||||
"terminal_review": assessment.get("terminal_review"),
|
||||
"worktree_state": assessment.get("worktree_state"),
|
||||
"owner_session_evidence": assessment.get("owner_session_evidence"),
|
||||
"cleanup_tool": assessment.get("cleanup_tool"),
|
||||
"required_confirmation": assessment.get("required_confirmation"),
|
||||
"active_lease": assessment.get("active_lease"),
|
||||
"no_steal_or_adoption": True,
|
||||
"no_repoint": True,
|
||||
"no_validation_transfer": True,
|
||||
"mode": "apply" if apply else "read_only",
|
||||
"reasons": assessment.get("reasons") or [],
|
||||
"forbidden": assessment.get("forbidden") or [],
|
||||
}
|
||||
|
||||
if not apply:
|
||||
return report
|
||||
if not assessment.get("cleanup_allowed"):
|
||||
report["success"] = False
|
||||
report["cleanup_skipped_reason"] = (
|
||||
assessment.get("fail_closed_reasons")
|
||||
or assessment.get("reasons")
|
||||
or ["cleanup not allowed"]
|
||||
)
|
||||
return report
|
||||
|
||||
comment_block = _profile_operation_gate("gitea.pr.comment")
|
||||
if comment_block:
|
||||
report["success"] = False
|
||||
report["reasons"] = comment_block
|
||||
report["permission_report"] = _permission_block_report("gitea.pr.comment")
|
||||
return report
|
||||
|
||||
verify_preflight_purity(remote)
|
||||
# Terminal lease marker (preserves history; does not delete comment 10749-style
|
||||
# entries — newest released marker neutralises the ledger).
|
||||
release_body = assessment.get("release_body") or ""
|
||||
audit_body = assessment.get("audit_comment_body") or ""
|
||||
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": "cleanup_obsolete_reviewer_comment_lease",
|
||||
"classification": assessment.get("classification"),
|
||||
},
|
||||
):
|
||||
posted_release = api_request(
|
||||
"POST", comment_url, auth, {"body": release_body}
|
||||
)
|
||||
posted_audit = None
|
||||
if audit_body.strip():
|
||||
posted_audit = api_request(
|
||||
"POST", comment_url, auth, {"body": audit_body}
|
||||
)
|
||||
|
||||
# Never seed session lease from cleaned foreign lease.
|
||||
report["cleanup_performed"] = True
|
||||
report["released_comment_id"] = (posted_release or {}).get("id")
|
||||
report["audit_comment_id"] = (posted_audit or {}).get("id")
|
||||
report["session_lease_seeded"] = False
|
||||
return report
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_release_reviewer_pr_lease(
|
||||
pr_number: int,
|
||||
|
||||
Reference in New Issue
Block a user