Files
Gitea-Tools/reviewer_mutation_workspace.py
T
sysadminandClaude Opus 4.8 dad1dc8d51 fix: bind reviewer mutations to active branches worktree (Closes #503)
Reviewer mutation tools now resolve workspace from worktree_path,
GITEA_ACTIVE_WORKTREE, or the session reviewer lease before preflight
and branches-only validation. Preflight rejects metadata-only worktree_path
bindings that would mislead runtime_context while mutations still inspect
the MCP process root.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-08 03:42:07 -04:00

157 lines
5.6 KiB
Python

"""Reviewer mutation workspace binding (#503).
Reviewer mutation tools must validate the same branches/ worktree that
read-only preflight and runtime_context inspect — not only the MCP server
process root.
"""
from __future__ import annotations
import os
import author_mutation_worktree as amw
ACTIVE_WORKTREE_ENV = amw.ACTIVE_WORKTREE_ENV
def resolve_reviewer_mutation_workspace(
*,
worktree_path: str | None,
worktree: str | None,
process_project_root: str,
active_worktree_env: str | None = None,
session_lease_worktree: str | None = None,
) -> str:
"""Resolve the workspace inspected before reviewer mutations."""
for candidate in (
worktree_path,
worktree,
active_worktree_env,
session_lease_worktree,
):
text = (candidate or "").strip()
if text:
return os.path.realpath(os.path.abspath(text))
return os.path.realpath(process_project_root)
def assess_metadata_only_worktree_binding(
*,
declared_worktree_path: str | None,
mutation_workspace: str,
process_project_root: str,
) -> dict:
"""Fail closed when declared worktree_path would not redirect mutations."""
declared = (declared_worktree_path or "").strip()
process_root = os.path.realpath(process_project_root)
mutation_root = os.path.realpath(mutation_workspace)
if not declared:
return {"block": False, "reasons": [], "metadata_only": False}
declared_root = os.path.realpath(os.path.abspath(declared))
if declared_root == mutation_root:
return {"block": False, "reasons": [], "metadata_only": False}
if declared_root != process_root and mutation_root == process_root:
return {
"block": True,
"metadata_only": True,
"reasons": [
"worktree_path is metadata-only for reviewer mutations: preflight "
f"inspected '{declared_root}' but mutation tools would still "
f"validate MCP server process root '{process_root}'"
],
"declared_worktree_path": declared_root,
"mutation_workspace": mutation_root,
"process_project_root": process_root,
}
return {"block": False, "reasons": [], "metadata_only": False}
def format_stale_mcp_workspace_binding_error(
*,
process_project_root: str,
mutation_workspace: str,
declared_worktree_path: str | None = None,
metadata_only: bool = False,
) -> str:
"""Canonical handoff when reviewer mutations bind to the wrong workspace."""
process_root = os.path.realpath(process_project_root)
mutation_root = os.path.realpath(mutation_workspace)
declared = (declared_worktree_path or "").strip()
declared_root = (
os.path.realpath(os.path.abspath(declared)) if declared else None
)
if metadata_only and declared_root:
lead = (
"Reviewer mutation workspace mismatch (#503): worktree_path affected "
f"read-only preflight only ('{declared_root}') while mutation tools "
f"still inspect MCP server process root '{process_root}'."
)
elif mutation_root == process_root:
lead = (
"Reviewer mutation workspace mismatch (#503): mutation tools are "
f"bound to the stable control checkout '{process_root}' instead of "
"an active branches/ review worktree."
)
else:
lead = (
"Reviewer mutation workspace mismatch (#503): mutation workspace "
f"'{mutation_root}' does not match the declared active task worktree."
)
fixes = (
"Fix: relaunch or reconnect the MCP server from the branches/ review "
"worktree, set GITEA_ACTIVE_WORKTREE to that path, pass worktree_path "
"to reviewer mutation tools, or acquire a reviewer PR lease from the "
"same branches/ worktree so mutations inherit the validated workspace."
)
return f"{lead} {fixes}"
def assess_reviewer_mutation_workspace(
*,
worktree_path: str | None,
worktree: str | None,
process_project_root: str,
active_worktree_env: str | None = None,
session_lease_worktree: str | None = None,
current_branch: str | None = None,
) -> dict:
"""Evaluate reviewer mutation workspace binding before preflight/mutation."""
mutation_workspace = resolve_reviewer_mutation_workspace(
worktree_path=worktree_path,
worktree=worktree,
process_project_root=process_project_root,
active_worktree_env=active_worktree_env,
session_lease_worktree=session_lease_worktree,
)
process_root = os.path.realpath(process_project_root)
metadata = assess_metadata_only_worktree_binding(
declared_worktree_path=worktree_path,
mutation_workspace=mutation_workspace,
process_project_root=process_root,
)
reasons = list(metadata.get("reasons") or [])
canonical_root = amw.resolve_canonical_repo_root(process_root, process_root)
branches = amw.assess_author_mutation_worktree(
workspace_path=mutation_workspace,
project_root=canonical_root,
current_branch=current_branch,
)
if branches["block"]:
reasons.extend(branches["reasons"])
block = bool(reasons)
return {
"block": block,
"reasons": reasons,
"mutation_workspace": mutation_workspace,
"process_project_root": process_root,
"canonical_repo_root": canonical_root,
"metadata_only": metadata.get("metadata_only", False),
"declared_worktree_path": metadata.get("declared_worktree_path"),
"under_branches": branches.get("under_branches", False),
}