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:
2026-07-19 14:43:10 -04:00
parent 8a851eb87e
commit 7f2b9f36de
6 changed files with 1168 additions and 15 deletions
+136
View File
@@ -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,