feat: add workflow dashboard for queue, leases, and next safe action (Closes #605)
Read-only MCP tool gitea_workflow_dashboard plus mcp-menu entry so operators and LLMs can see PR/issue queues, leases, terminal locks, blockers, and exact next-safe prompts without reconstructing state from comments. Never assigns work or presents blocked/terminal-locked items as safe.
This commit is contained in:
@@ -1768,6 +1768,7 @@ import allocator_service # noqa: E402
|
||||
import allocator_dependencies # noqa: E402
|
||||
import control_plane_db # noqa: E402
|
||||
import lease_lifecycle # noqa: E402
|
||||
import workflow_dashboard # noqa: E402 # #605 live queue/lease dashboard
|
||||
import incident_bridge # noqa: E402
|
||||
import sentry_observability # noqa: E402 (#606 optional Sentry observability)
|
||||
import agent_temp_artifacts
|
||||
@@ -18420,6 +18421,141 @@ def gitea_list_workflow_leases(
|
||||
return result
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_workflow_dashboard(
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
include_issues: bool = True,
|
||||
include_prs: bool = True,
|
||||
include_non_active_leases: bool = True,
|
||||
candidates_json: str | None = None,
|
||||
limit: int = 100,
|
||||
) -> dict:
|
||||
"""Read-only MCP workflow dashboard: queues, leases, blockers, next safe action (#605).
|
||||
|
||||
Machine-readable and human-readable. Never assigns work — exclusive work
|
||||
still requires ``gitea_allocate_next_work``. Never presents blocked or
|
||||
terminal-locked items as safe next work.
|
||||
|
||||
*candidates_json* may inject a JSON list of candidate dicts (tests /
|
||||
offline fixtures). When omitted, open issues/PRs are loaded from Gitea.
|
||||
Incomplete live inventory fails closed (no safe suggestions).
|
||||
"""
|
||||
read_block = _profile_operation_gate("gitea.read")
|
||||
if read_block:
|
||||
return {
|
||||
"success": False,
|
||||
"read_only": True,
|
||||
"reasons": read_block,
|
||||
"permission_report": _permission_block_report("gitea.read"),
|
||||
"primary_next_safe_action": None,
|
||||
}
|
||||
|
||||
try:
|
||||
h, o, r = _resolve(remote, host, org, repo)
|
||||
except ValueError as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"read_only": True,
|
||||
"reasons": [str(exc)],
|
||||
"primary_next_safe_action": None,
|
||||
}
|
||||
|
||||
inv_reasons: list[str] = []
|
||||
candidates: list[Any] = []
|
||||
inventory_complete = True
|
||||
if candidates_json:
|
||||
try:
|
||||
raw = json.loads(candidates_json)
|
||||
if not isinstance(raw, list):
|
||||
raise ValueError("candidates_json must be a JSON list")
|
||||
for item in raw:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
candidates.append(allocator_service.candidate_from_dict(item))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {
|
||||
"success": False,
|
||||
"read_only": True,
|
||||
"inventory_complete": False,
|
||||
"reasons": [
|
||||
f"invalid candidates_json: {_redact(str(exc))} (fail closed)"
|
||||
],
|
||||
"primary_next_safe_action": None,
|
||||
}
|
||||
else:
|
||||
candidates, inv_reasons, inventory_complete = _allocator_candidates_from_gitea(
|
||||
remote=remote,
|
||||
host=host,
|
||||
org=o,
|
||||
repo=r,
|
||||
include_issues=include_issues,
|
||||
include_prs=include_prs,
|
||||
)
|
||||
|
||||
leases: list[dict] = []
|
||||
terminal_pr: int | None = None
|
||||
terminal_lock: dict | None = None
|
||||
db, db_errs = _control_plane_db_or_error()
|
||||
if db is None:
|
||||
inv_reasons.extend(db_errs or ["control-plane DB unavailable"])
|
||||
# Leases/terminal lock unavailable — still render queues fail-closed
|
||||
# for safe suggestions when inventory is incomplete; when inventory is
|
||||
# complete, surface lease gap in reasons without inventing leases.
|
||||
inv_reasons.append(
|
||||
"control-plane DB unavailable; lease/terminal sections empty"
|
||||
)
|
||||
else:
|
||||
try:
|
||||
lease_result = lease_lifecycle.list_active_leases(
|
||||
db,
|
||||
remote=remote if remote in REMOTES else remote,
|
||||
org=o,
|
||||
repo=r,
|
||||
role=None,
|
||||
include_non_active=bool(include_non_active_leases),
|
||||
limit=max(1, int(limit)),
|
||||
)
|
||||
leases = list(lease_result.get("leases") or [])
|
||||
except Exception as exc: # noqa: BLE001
|
||||
inv_reasons.append(
|
||||
f"lease listing failed: {_redact(str(exc))}"
|
||||
)
|
||||
try:
|
||||
terminal = db.get_active_terminal_lock(
|
||||
remote=remote if remote in REMOTES else remote,
|
||||
org=o,
|
||||
repo=r,
|
||||
)
|
||||
if terminal:
|
||||
terminal_lock = dict(terminal)
|
||||
terminal_pr = int(terminal["terminal_pr"])
|
||||
except Exception as exc: # noqa: BLE001
|
||||
inv_reasons.append(
|
||||
f"terminal lock lookup failed: {_redact(str(exc))}"
|
||||
)
|
||||
|
||||
snap = workflow_dashboard.build_workflow_dashboard(
|
||||
candidates=candidates,
|
||||
remote=remote if remote in REMOTES else remote,
|
||||
org=o,
|
||||
repo=r,
|
||||
leases=leases,
|
||||
terminal_pr=terminal_pr,
|
||||
terminal_lock=terminal_lock,
|
||||
inventory_complete=inventory_complete,
|
||||
inventory_reasons=inv_reasons,
|
||||
)
|
||||
payload = snap.as_dict()
|
||||
payload["inventory_source"] = (
|
||||
"candidates_json" if candidates_json else "gitea_live"
|
||||
)
|
||||
payload["limit_applies_to"] = "lease_listing_only"
|
||||
return payload
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_inspect_workflow_lease(
|
||||
lease_id: str,
|
||||
|
||||
Reference in New Issue
Block a user