feat: add reconciler supersession close gate

This commit is contained in:
2026-07-09 12:36:50 -04:00
parent c738e5b10d
commit b0ae1605c3
6 changed files with 706 additions and 0 deletions
+253
View File
@@ -5725,6 +5725,259 @@ def gitea_reconcile_already_landed_pr(
return result
@mcp.tool()
def gitea_reconcile_superseded_by_merged_pr(
target_pr_number: int,
superseding_pr_number: int,
target_issue_number: int | None = None,
target_independently_required: bool | None = None,
issue_satisfied_by_superseding: bool = False,
close_pr: bool = False,
close_issue: bool = False,
post_comment: bool = False,
comment_body: str = "",
target_branch: str = "master",
remote: str = "dadeschools",
host: str | None = None,
org: str | None = None,
repo: str | None = None,
) -> dict:
"""Reconcile a PR/issue superseded by a different merged PR (#525).
The tool always performs live-state assessment. Mutations require a
reconciler-capable profile, a merged superseding PR whose head is on the
freshly fetched target branch, explicit proof that the target PR has no
independent remaining work, and a canonical close comment that cites the
superseding PR and merge commit.
"""
read_block = _profile_operation_gate("gitea.read")
if read_block:
return {
"success": False,
"performed": False,
"reasons": read_block,
"permission_report": _permission_block_report("gitea.read"),
}
close_pr_capability = True
close_issue_capability = True
if close_pr:
close_block = _profile_operation_gate("gitea.pr.close")
close_pr_capability = not bool(close_block)
if close_block:
return {
"success": False,
"performed": False,
"target_pr_number": target_pr_number,
"required_permission": "gitea.pr.close",
"reasons": close_block,
"permission_report": _permission_block_report("gitea.pr.close"),
"safe_next_action": (
"Launch the prgs-reconciler MCP namespace or configure a "
"profile with gitea.pr.close for supersession cleanup."
),
}
if close_issue:
issue_block = _profile_operation_gate("gitea.issue.close")
close_issue_capability = not bool(issue_block)
if issue_block:
return {
"success": False,
"performed": False,
"target_issue_number": target_issue_number,
"required_permission": "gitea.issue.close",
"reasons": issue_block,
"permission_report": _permission_block_report("gitea.issue.close"),
}
if post_comment:
comment_block = _profile_operation_gate("gitea.pr.comment")
if comment_block:
return {
"success": False,
"performed": False,
"target_pr_number": target_pr_number,
"required_permission": "gitea.pr.comment",
"reasons": comment_block,
"permission_report": _permission_block_report("gitea.pr.comment"),
}
h, o, r = _resolve(remote, host, org, repo)
auth = _auth(h)
base = repo_api_url(h, o, r)
target_pr = api_request("GET", f"{base}/pulls/{target_pr_number}", auth)
superseding_pr = api_request(
"GET", f"{base}/pulls/{superseding_pr_number}", auth
)
target_fetch = already_landed_reconcile.fetch_target_branch(
PROJECT_ROOT, remote, target_branch
)
superseding_head_sha = (
(superseding_pr.get("head") or {}).get("sha")
if isinstance(superseding_pr.get("head"), dict)
else None
)
ancestor = None
if target_fetch.get("success"):
ancestor = already_landed_reconcile.is_head_ancestor_of_ref(
PROJECT_ROOT,
superseding_head_sha,
target_fetch.get("target_ref") or f"{remote}/{target_branch}",
)
issue_state = None
if target_issue_number:
issue = api_request("GET", f"{base}/issues/{target_issue_number}", auth)
issue_state = issue.get("state")
stripped_comment = comment_body.strip()
canonical_gate = (
_canonical_comment_gate(stripped_comment)
if stripped_comment
else {"blocked": True, "canonical_comment_validation": None}
)
merge_commit_sha = (superseding_pr.get("merge_commit_sha") or "").strip()
comment_mentions_superseding = (
f"#{superseding_pr_number}" in stripped_comment
or f"PR {superseding_pr_number}" in stripped_comment
)
comment_mentions_merge = bool(
merge_commit_sha and merge_commit_sha in stripped_comment
)
gate = review_proofs.assess_reconciler_supersession_close_gate(
target_pr_number=target_pr_number,
target_pr_state=target_pr.get("state"),
target_pr_mergeable=target_pr.get("mergeable"),
target_independently_required=target_independently_required,
superseding_pr_number=superseding_pr_number,
superseding_pr_state=superseding_pr.get("state"),
superseding_pr_merged=bool(superseding_pr.get("merged")),
superseding_merge_commit_sha=merge_commit_sha,
superseding_head_sha=superseding_head_sha,
target_branch=target_branch,
target_branch_sha=target_fetch.get("target_branch_sha"),
superseding_head_is_ancestor_of_target=ancestor,
canonical_comment_valid=not canonical_gate["blocked"],
canonical_comment_mentions_superseding_pr=comment_mentions_superseding,
canonical_comment_mentions_merge_commit=comment_mentions_merge,
close_pr_capability=close_pr_capability,
close_issue_capability=close_issue_capability,
target_issue_state=issue_state,
issue_satisfied_by_superseding=issue_satisfied_by_superseding,
)
result: dict = {
"success": gate["pr_close_allowed"],
"performed": False,
"target_pr_number": target_pr_number,
"superseding_pr_number": superseding_pr_number,
"target_issue_number": target_issue_number,
"target_pr_state": target_pr.get("state"),
"target_pr_mergeable": target_pr.get("mergeable"),
"target_independently_required": target_independently_required,
"superseding_pr_state": superseding_pr.get("state"),
"superseding_pr_merged": bool(superseding_pr.get("merged")),
"superseding_merge_commit_sha": merge_commit_sha,
"superseding_head_sha": superseding_head_sha,
"target_branch": target_branch,
"target_branch_sha": target_fetch.get("target_branch_sha"),
"superseding_head_is_ancestor_of_target": ancestor,
"linked_issue_live_status": issue_state,
"gate": gate,
"reasons": list(gate.get("reasons") or []),
"canonical_comment_validation": canonical_gate.get(
"canonical_comment_validation"
),
"pr_comment_posted": False,
"pr_closed": False,
"issue_closed": False,
}
wants_mutation = close_pr or close_issue or post_comment
if not gate["pr_close_allowed"]:
result["safe_next_action"] = gate.get("safe_next_action")
return result
if close_issue and not gate["issue_close_allowed"]:
result["success"] = False
result["safe_next_action"] = (
"do not mutate; linked issue close was requested but the "
"supersession gate did not allow issue closure"
)
return result
if not wants_mutation:
result["success"] = True
result["safe_next_action"] = "rerun with explicit mutation flags"
return result
verify_preflight_purity(remote, task="reconcile_close_superseded_pr")
if post_comment:
comment_url = f"{base}/issues/{target_pr_number}/comments"
with _audited(
"comment_pr",
host=h,
remote=remote,
org=o,
repo=r,
pr_number=target_pr_number,
request_metadata={"source": "reconcile_superseded_by_merged_pr"},
):
api_request("POST", comment_url, auth, {"body": stripped_comment})
result["pr_comment_posted"] = True
result["performed"] = True
if close_pr:
with _audited(
"close_pr",
host=h,
remote=remote,
org=o,
repo=r,
pr_number=target_pr_number,
request_metadata={
"source": "reconcile_superseded_by_merged_pr",
"superseding_pr_number": superseding_pr_number,
"superseding_merge_commit_sha": merge_commit_sha,
"target_branch_sha": target_fetch.get("target_branch_sha"),
},
):
closed = api_request(
"PATCH",
f"{base}/pulls/{target_pr_number}",
auth,
{"state": "closed"},
)
result["target_pr_state"] = closed.get("state", "closed")
result["pr_closed"] = True
result["performed"] = True
if close_issue and target_issue_number and gate["issue_close_allowed"]:
with _audited(
"close_issue",
host=h,
remote=remote,
org=o,
repo=r,
issue_number=target_issue_number,
request_metadata={
"source": "reconcile_superseded_by_merged_pr",
"superseding_pr_number": superseding_pr_number,
"superseding_merge_commit_sha": merge_commit_sha,
},
):
api_request(
"PATCH",
f"{base}/issues/{target_issue_number}",
auth,
{"state": "closed"},
)
result["issue_closed"] = True
result["performed"] = True
return result
@mcp.tool()
def gitea_close_issue(
issue_number: int,