feat: enforce live-namespace block in review/merge state machine (#543 AC5)

The namespace health check (05fdcee) classified a false-ready namespace but
only returned an advisory blocks_merge_workflow flag; nothing in the merge
gate consumed it. Wire it in so a broken live namespace hard-blocks merge.

- review_merge_state_machine.assess_workflow_blockers: add live_namespace_broken
  blocker (registered-in-FastMCP but not callable-through-namespace).
- assess_state_advancement: forward **blocker_kwargs so can_approve/can_merge/
  workflow_status honor the full blocker set (also fixes latent drop of
  mcp_reconnect_failed/stale_capability_state through those paths).
- gitea_assess_review_merge_state_machine tool: accept live_namespace_broken and
  thread it through all state-machine calls.
- Tests: prove can_merge/workflow_status/tool block on live_namespace_broken even
  with every review state + pre-merge gate satisfied; bridge classify verdict.
- Docs: enforcement section wiring blocks_merge_workflow -> live_namespace_broken.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
2026-07-09 18:39:41 -04:00
co-authored by Claude Opus 4.8
parent 314615ec2c
commit 93781af7e9
4 changed files with 135 additions and 21 deletions
+25 -8
View File
@@ -93,8 +93,16 @@ def assess_workflow_blockers(
capability_blocked: bool = False,
mcp_reconnect_failed: bool = False,
stale_capability_state: bool = False,
live_namespace_broken: bool = False,
) -> dict[str, Any]:
"""Return hard blockers that forbid all PR queue work (#290 AC3)."""
"""Return hard blockers that forbid all PR queue work (#290 AC3).
``live_namespace_broken`` fails the merge/review path closed when the live
MCP namespace call path is unusable (e.g. ``client is closing: EOF``) even
though the tool is registered in FastMCP (#543 AC5). Feed it the
``blocks_merge_workflow`` verdict from
``mcp_namespace_health.classify_namespace_probe``.
"""
reasons: list[str] = []
if infra_stop:
reasons.append("infra_stop is active; PR selection/review/merge is forbidden")
@@ -104,6 +112,12 @@ def assess_workflow_blockers(
reasons.append("MCP reconnect failed; stale session state cannot be reused")
if stale_capability_state:
reasons.append("stale MCP capability state detected after reconnect failure")
if live_namespace_broken:
reasons.append(
"live MCP namespace call path is broken (registered in FastMCP but "
"not callable through the namespace); repair the namespace before "
"review/merge"
)
return {
"block": bool(reasons),
"reasons": reasons,
@@ -118,16 +132,19 @@ def assess_state_advancement(
state_completion: dict[str, bool] | None,
*,
target_state: str,
infra_stop: bool = False,
capability_blocked: bool = False,
**blocker_kwargs,
) -> dict[str, Any]:
"""Fail closed when *target_state* is requested before upstream gates pass."""
"""Fail closed when *target_state* is requested before upstream gates pass.
Forwards every blocker flag (``infra_stop``, ``capability_blocked``,
``mcp_reconnect_failed``, ``stale_capability_state``,
``live_namespace_broken``) to :func:`assess_workflow_blockers` so
``can_approve``/``can_merge`` honor the full blocker set, not just
infra_stop/capability_blocked (#543 AC5).
"""
completion = dict(state_completion or {})
target = _clean(target_state).upper()
blockers = assess_workflow_blockers(
infra_stop=infra_stop,
capability_blocked=capability_blocked,
)
blockers = assess_workflow_blockers(**blocker_kwargs)
reasons = list(blockers["reasons"])
try: