feat: add reconciliation workflow MCP tools for already-landed PRs (Closes #301)
Expose read-only assess/scan tools and capability planning so already-landed open PRs can be reconciled without review or merge. Register the gitea-reconcile-landed-pr skill and workflow-source verification.
This commit is contained in:
@@ -477,6 +477,7 @@ import review_proofs # noqa: E402
|
||||
import agent_temp_artifacts
|
||||
import issue_lock_worktree # noqa: E402
|
||||
import merged_cleanup_reconcile # noqa: E402
|
||||
import reconciliation_workflow # noqa: E402
|
||||
|
||||
|
||||
# Fail-closed exact-issue-lock file (#204): written by gitea_lock_issue,
|
||||
@@ -3425,6 +3426,178 @@ def gitea_reconcile_merged_cleanups(
|
||||
return {"success": True, "performed": True, **report}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_assess_already_landed_reconciliation(
|
||||
pr_number: int,
|
||||
target_branch: str = "master",
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
) -> dict:
|
||||
"""Read-only: assess already-landed reconciliation for one open PR (#301).
|
||||
|
||||
Fetches the PR live, refreshes the target branch, checks ancestor proof,
|
||||
and returns a capability plan for the active profile. Does not review,
|
||||
approve, request changes, merge, or mutate Gitea state.
|
||||
|
||||
Args:
|
||||
pr_number: Open PR to assess.
|
||||
target_branch: Branch used for ancestry proof (default master).
|
||||
remote: Known instance — 'dadeschools' or 'prgs'.
|
||||
host: Override the Gitea host.
|
||||
org: Override the owner/organization.
|
||||
repo: Override the repository name.
|
||||
|
||||
Returns:
|
||||
dict with eligibility proof, capability plan, and safe next action.
|
||||
"""
|
||||
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"),
|
||||
}
|
||||
|
||||
h, o, r = _resolve(remote, host, org, repo)
|
||||
auth = _auth(h)
|
||||
pr = api_request("GET", f"{repo_api_url(h, o, r)}/pulls/{pr_number}", auth)
|
||||
|
||||
assessment = reconciliation_workflow.assess_open_pr_reconciliation(
|
||||
pr=pr,
|
||||
project_root=PROJECT_ROOT,
|
||||
remote=remote,
|
||||
target_branch=target_branch,
|
||||
)
|
||||
|
||||
profile = get_profile()
|
||||
capabilities = reconciliation_workflow.profile_reconciliation_capabilities(
|
||||
profile.get("allowed_operations"),
|
||||
profile.get("forbidden_operations"),
|
||||
)
|
||||
plan = reconciliation_workflow.resolve_reconciliation_plan(
|
||||
assessment=assessment,
|
||||
capabilities=capabilities,
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"performed": False,
|
||||
"pr_number": pr_number,
|
||||
"assessment": assessment,
|
||||
"capabilities": capabilities,
|
||||
"plan": plan,
|
||||
"workflow_source": "skills/llm-project-workflow/workflows/reconcile-landed-pr.md",
|
||||
"task_mode": "reconcile-landed-pr",
|
||||
"review_merge_allowed": False,
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_scan_already_landed_open_prs(
|
||||
target_branch: str = "master",
|
||||
limit: int = 50,
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
) -> dict:
|
||||
"""Read-only: list open PRs already landed on the target branch (#301).
|
||||
|
||||
Traverses open PR inventory with pagination proof, assesses each candidate,
|
||||
and returns PRs classified as ``ALREADY_LANDED_RECONCILE_REQUIRED``. Does
|
||||
not review, approve, merge, or mutate Gitea state.
|
||||
|
||||
Args:
|
||||
target_branch: Branch used for ancestry proof (default master).
|
||||
limit: Max open PRs to inspect across pages.
|
||||
remote: Known instance — 'dadeschools' or 'prgs'.
|
||||
host: Override the Gitea host.
|
||||
org: Override the owner/organization.
|
||||
repo: Override the repository name.
|
||||
|
||||
Returns:
|
||||
dict with candidates, pagination metadata, and target branch SHA proof.
|
||||
"""
|
||||
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"),
|
||||
}
|
||||
|
||||
h, o, r = _resolve(remote, host, org, repo)
|
||||
auth = _auth(h)
|
||||
url = f"{repo_api_url(h, o, r)}/pulls?state=open"
|
||||
|
||||
open_prs: list[dict] = []
|
||||
current_page = 1
|
||||
pages_fetched = 0
|
||||
last_meta: dict = {}
|
||||
per_page = min(50, max(1, int(limit)))
|
||||
while pages_fetched < 100 and len(open_prs) < limit:
|
||||
raw_page, last_meta = api_fetch_page(
|
||||
url, auth, page=current_page, limit=per_page
|
||||
)
|
||||
pages_fetched += 1
|
||||
remaining = limit - len(open_prs)
|
||||
open_prs.extend(raw_page[:remaining])
|
||||
if last_meta["is_final_page"] or len(open_prs) >= limit:
|
||||
break
|
||||
current_page += 1
|
||||
|
||||
pagination = {
|
||||
"page": 1,
|
||||
"per_page": per_page,
|
||||
"returned_count": len(open_prs),
|
||||
"has_more": False,
|
||||
"next_page": None,
|
||||
"is_final_page": bool(last_meta.get("is_final_page")),
|
||||
"pages_fetched": pages_fetched,
|
||||
"inventory_complete": bool(last_meta.get("is_final_page")),
|
||||
"total_count": len(open_prs) if last_meta.get("is_final_page") else None,
|
||||
}
|
||||
|
||||
target_fetch = reconciliation_workflow.fetch_target_branch(
|
||||
PROJECT_ROOT, remote, target_branch
|
||||
)
|
||||
|
||||
candidates: list[dict] = []
|
||||
for pr in open_prs:
|
||||
assessment = reconciliation_workflow.assess_open_pr_reconciliation(
|
||||
pr=pr,
|
||||
project_root=PROJECT_ROOT,
|
||||
remote=remote,
|
||||
target_branch=target_branch,
|
||||
target_fetch=target_fetch,
|
||||
)
|
||||
if assessment.get("eligibility_class") == (
|
||||
reconciliation_workflow.ELIGIBILITY_ALREADY_LANDED
|
||||
):
|
||||
candidates.append(assessment)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"performed": False,
|
||||
"target_branch": target_branch,
|
||||
"target_branch_sha": target_fetch.get("target_branch_sha"),
|
||||
"git_ref_mutations": (
|
||||
[target_fetch["git_fetch_command"]]
|
||||
if target_fetch.get("git_fetch_command")
|
||||
else []
|
||||
),
|
||||
"candidates": candidates,
|
||||
"candidate_count": len(candidates),
|
||||
"pagination": pagination,
|
||||
"task_mode": "reconcile-landed-pr",
|
||||
"review_merge_allowed": False,
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_close_issue(
|
||||
issue_number: int,
|
||||
@@ -4182,6 +4355,28 @@ _PROJECT_SKILLS = {
|
||||
"Do not merge unless the operator explicitly authorizes it.",
|
||||
],
|
||||
},
|
||||
"gitea-reconcile-landed-pr": {
|
||||
"description": "Reconcile open PRs whose heads are already on the "
|
||||
"target branch without review or merge.",
|
||||
"when_to_use": "An open PR blocks the review queue but its head SHA "
|
||||
"is already an ancestor of master; use the dedicated "
|
||||
"reconciliation path instead of review/merge.",
|
||||
"required_operations": ["gitea.read"],
|
||||
"status": "available",
|
||||
"notes": "Load skills/llm-project-workflow/workflows/reconcile-landed-pr.md "
|
||||
"first. PR close requires exact gitea.pr.close; review/merge "
|
||||
"capabilities must not be used on this path.",
|
||||
"steps": [
|
||||
"Resolve task: gitea_resolve_task_capability(task='reconcile_landed_pr').",
|
||||
"Load canonical workflow via mcp_get_skill_guide or "
|
||||
"skills/llm-project-workflow/workflows/reconcile-landed-pr.md.",
|
||||
"Scan queue: gitea_scan_already_landed_open_prs (pagination proof).",
|
||||
"Assess one PR: gitea_assess_already_landed_reconciliation.",
|
||||
"Follow the plan outcome: full close only with gitea.pr.close, "
|
||||
"comment-then-stop when close is missing, recovery handoff otherwise.",
|
||||
"Never approve, request changes, or merge on this path.",
|
||||
],
|
||||
},
|
||||
"gitea-pr-merge": {
|
||||
"description": "Gated merge of an approved pull request.",
|
||||
"when_to_use": "Reviewer/merger profile with explicit operator "
|
||||
|
||||
Reference in New Issue
Block a user