fix: resolve conflicts for PR #381 against latest master
This commit is contained in:
+274
-1
@@ -477,6 +477,7 @@ import review_proofs # noqa: E402
|
||||
import agent_temp_artifacts
|
||||
import issue_lock_worktree # noqa: E402
|
||||
import already_landed_reconcile # noqa: E402
|
||||
import issue_claim_heartbeat # noqa: E402
|
||||
import merged_cleanup_reconcile # noqa: E402
|
||||
|
||||
|
||||
@@ -5338,6 +5339,41 @@ def gitea_audit_config() -> dict:
|
||||
return report
|
||||
|
||||
|
||||
def _post_structured_issue_comment(
|
||||
*,
|
||||
issue_number: int,
|
||||
body: str,
|
||||
remote: str,
|
||||
host: str | None,
|
||||
org: str | None,
|
||||
repo: str | None,
|
||||
audit_op: str = "create_issue_comment",
|
||||
) -> dict:
|
||||
"""Post an issue-thread comment after permission gates already passed."""
|
||||
h, o, r = _resolve(remote, host, org, repo)
|
||||
auth = _auth(h)
|
||||
api = f"{repo_api_url(h, o, r)}/issues/{issue_number}/comments"
|
||||
with _audited(
|
||||
audit_op,
|
||||
host=h,
|
||||
remote=remote,
|
||||
org=o,
|
||||
repo=r,
|
||||
issue_number=issue_number,
|
||||
request_metadata={"body_chars": len(body)},
|
||||
):
|
||||
data = api_request("POST", api, auth, {"body": body})
|
||||
result = {
|
||||
"success": True,
|
||||
"performed": True,
|
||||
"comment_id": data["id"],
|
||||
"issue_number": issue_number,
|
||||
}
|
||||
if _reveal_endpoints():
|
||||
result["url"] = data.get("html_url")
|
||||
return result
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_mark_issue(
|
||||
issue_number: int,
|
||||
@@ -5347,6 +5383,8 @@ def gitea_mark_issue(
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
worktree_path: str | None = None,
|
||||
branch_name: str | None = None,
|
||||
profile: str | None = None,
|
||||
) -> dict:
|
||||
"""Claim or release an issue via the status:in-progress label.
|
||||
|
||||
@@ -5396,7 +5434,31 @@ def gitea_mark_issue(
|
||||
request_metadata={"op": "add", "label": "status:in-progress"}):
|
||||
api_request("POST", f"{base}/issues/{issue_number}/labels", auth,
|
||||
{"labels": [label_id]})
|
||||
return {"success": True, "message": f"Issue #{issue_number} claimed."}
|
||||
active_profile = (profile or get_profile().get("profile_name") or "unknown")
|
||||
branch = (branch_name or "pending").strip() or "pending"
|
||||
heartbeat_body = issue_claim_heartbeat.format_heartbeat_body(
|
||||
kind="claim",
|
||||
issue_number=issue_number,
|
||||
branch=branch,
|
||||
phase="claimed",
|
||||
profile=active_profile,
|
||||
next_action="create worktree and begin implementation",
|
||||
)
|
||||
heartbeat = _post_structured_issue_comment(
|
||||
issue_number=issue_number,
|
||||
body=heartbeat_body,
|
||||
remote=remote,
|
||||
host=host,
|
||||
org=org,
|
||||
repo=repo,
|
||||
audit_op="claim_heartbeat",
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Issue #{issue_number} claimed.",
|
||||
"heartbeat_posted": heartbeat.get("success", False),
|
||||
"heartbeat_comment_id": heartbeat.get("comment_id"),
|
||||
}
|
||||
else:
|
||||
with _audited("unlabel_issue", host=h, remote=remote, org=o, repo=r,
|
||||
issue_number=issue_number,
|
||||
@@ -5406,6 +5468,217 @@ def gitea_mark_issue(
|
||||
return {"success": True, "message": f"Issue #{issue_number} released."}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_post_heartbeat(
|
||||
issue_number: int,
|
||||
branch: str,
|
||||
phase: str,
|
||||
pr: str = "none",
|
||||
next_action: str = "none",
|
||||
blocker: str = "none",
|
||||
profile: str | None = None,
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
) -> dict:
|
||||
"""Post a structured progress heartbeat on a claimed issue (#268)."""
|
||||
blocked = _profile_permission_block(
|
||||
task_capability_map.required_permission("post_heartbeat"))
|
||||
if blocked:
|
||||
return blocked
|
||||
verify_preflight_purity(remote)
|
||||
active_profile = profile or get_profile().get("profile_name")
|
||||
body = issue_claim_heartbeat.format_heartbeat_body(
|
||||
kind="progress",
|
||||
issue_number=issue_number,
|
||||
branch=branch,
|
||||
phase=phase,
|
||||
profile=active_profile,
|
||||
pr=pr,
|
||||
next_action=next_action,
|
||||
blocker=blocker,
|
||||
)
|
||||
return _post_structured_issue_comment(
|
||||
issue_number=issue_number,
|
||||
body=body,
|
||||
remote=remote,
|
||||
host=host,
|
||||
org=org,
|
||||
repo=repo,
|
||||
audit_op="progress_heartbeat",
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_reconcile_issue_claims(
|
||||
state: str = "open",
|
||||
stale_after_hours: int = 24,
|
||||
heartbeat_lease_minutes: int = 30,
|
||||
limit: int = 100,
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
) -> dict:
|
||||
"""Read-only inventory of issue claims and heartbeat lease status (#268)."""
|
||||
read_block = _profile_operation_gate("gitea.read")
|
||||
if read_block:
|
||||
return {
|
||||
"success": False,
|
||||
"reasons": read_block,
|
||||
"permission_report": _permission_block_report("gitea.read"),
|
||||
}
|
||||
|
||||
h, o, r = _resolve(remote, host, org, repo)
|
||||
auth = _auth(h)
|
||||
base = repo_api_url(h, o, r)
|
||||
issues = api_get_all(f"{base}/issues?state={state}&type=issues", auth, limit=limit)
|
||||
open_prs = api_get_all(f"{base}/pulls?state=open", auth)
|
||||
branches = api_get_all(f"{base}/branches", auth, limit=limit)
|
||||
branch_names = [b.get("name") for b in branches if b.get("name")]
|
||||
|
||||
comments_by_issue: dict[int, list[dict]] = {}
|
||||
for issue in issues:
|
||||
if not issue_claim_heartbeat.issue_has_in_progress_label(issue):
|
||||
continue
|
||||
number = int(issue["number"])
|
||||
api = f"{base}/issues/{number}/comments"
|
||||
comments_by_issue[number] = api_request("GET", api, auth) or []
|
||||
|
||||
reclaim_after_minutes = max(heartbeat_lease_minutes * 2, 60)
|
||||
inventory = issue_claim_heartbeat.build_claim_inventory(
|
||||
issues=issues,
|
||||
comments_by_issue=comments_by_issue,
|
||||
open_prs=open_prs,
|
||||
branch_names=branch_names,
|
||||
heartbeat_lease_minutes=heartbeat_lease_minutes,
|
||||
reclaim_after_minutes=reclaim_after_minutes,
|
||||
)
|
||||
inventory["cleanup_plan"] = issue_claim_heartbeat.build_cleanup_plan(inventory)
|
||||
inventory["success"] = True
|
||||
inventory["performed"] = False
|
||||
return inventory
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_cleanup_stale_claims(
|
||||
dry_run: bool = True,
|
||||
execute_confirmed: bool = False,
|
||||
heartbeat_lease_minutes: int = 30,
|
||||
limit: int = 100,
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
) -> dict:
|
||||
"""Propose or execute stale-claim cleanup for phantom/reclaimable issues (#268)."""
|
||||
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"),
|
||||
}
|
||||
|
||||
inventory = gitea_reconcile_issue_claims(
|
||||
heartbeat_lease_minutes=heartbeat_lease_minutes,
|
||||
limit=limit,
|
||||
remote=remote,
|
||||
host=host,
|
||||
org=org,
|
||||
repo=repo,
|
||||
)
|
||||
if not inventory.get("success"):
|
||||
return inventory
|
||||
|
||||
plan = [
|
||||
entry
|
||||
for entry in inventory.get("cleanup_plan") or []
|
||||
if entry.get("action") == "remove_status_in_progress_and_comment"
|
||||
]
|
||||
report = {
|
||||
"success": True,
|
||||
"dry_run": dry_run,
|
||||
"performed": False,
|
||||
"planned_actions": plan,
|
||||
"inventory_counts": inventory.get("counts"),
|
||||
}
|
||||
if dry_run:
|
||||
return report
|
||||
|
||||
if not execute_confirmed:
|
||||
raise ValueError(
|
||||
"execute_confirmed must be True when dry_run=False (fail closed)"
|
||||
)
|
||||
|
||||
blocked = _profile_permission_block(
|
||||
task_capability_map.required_permission("cleanup_stale_claims"))
|
||||
if blocked:
|
||||
return blocked
|
||||
verify_preflight_purity(remote)
|
||||
|
||||
h, o, r = _resolve(remote, host, org, repo)
|
||||
auth = _auth(h)
|
||||
base = repo_api_url(h, o, r)
|
||||
labels = api_request("GET", f"{base}/labels?limit=100", auth)
|
||||
label_id = next(
|
||||
(lb["id"] for lb in labels if lb.get("name") == "status:in-progress"),
|
||||
None,
|
||||
)
|
||||
if label_id is None:
|
||||
raise RuntimeError("Label 'status:in-progress' not found")
|
||||
|
||||
actions: list[dict] = []
|
||||
active_profile = get_profile().get("profile_name")
|
||||
for entry in plan:
|
||||
issue_number = int(entry["issue_number"])
|
||||
with _audited(
|
||||
"unlabel_issue",
|
||||
host=h,
|
||||
remote=remote,
|
||||
org=o,
|
||||
repo=r,
|
||||
issue_number=issue_number,
|
||||
request_metadata={"op": "cleanup_stale_claim", "label": "status:in-progress"},
|
||||
):
|
||||
api_request(
|
||||
"DELETE",
|
||||
f"{base}/issues/{issue_number}/labels/{label_id}",
|
||||
auth,
|
||||
)
|
||||
cleanup_body = issue_claim_heartbeat.format_heartbeat_body(
|
||||
kind="cleanup",
|
||||
issue_number=issue_number,
|
||||
branch="none",
|
||||
phase="stale-claim-cleanup",
|
||||
profile=active_profile,
|
||||
next_action="issue reclaimable by queue",
|
||||
blocker=entry.get("status") or "stale",
|
||||
)
|
||||
comment = _post_structured_issue_comment(
|
||||
issue_number=issue_number,
|
||||
body=cleanup_body,
|
||||
remote=remote,
|
||||
host=host,
|
||||
org=org,
|
||||
repo=repo,
|
||||
audit_op="cleanup_heartbeat",
|
||||
)
|
||||
actions.append(
|
||||
{
|
||||
"issue_number": issue_number,
|
||||
"label_removed": True,
|
||||
"cleanup_comment_id": comment.get("comment_id"),
|
||||
}
|
||||
)
|
||||
|
||||
report["performed"] = True
|
||||
report["actions"] = actions
|
||||
return report
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_list_labels(
|
||||
remote: str = "dadeschools",
|
||||
|
||||
Reference in New Issue
Block a user