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]>
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
"""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),
|
||||
}
|
||||
Reference in New Issue
Block a user