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:
2026-07-08 03:42:07 -04:00
co-authored by Claude Opus 4.8
parent 9ada4762c5
commit dad1dc8d51
3 changed files with 508 additions and 5 deletions
+134 -5
View File
@@ -208,6 +208,71 @@ def _resolve_author_mutation_context(worktree_path: str | None = None) -> dict:
)
def _reviewer_session_worktree() -> str | None:
session = reviewer_pr_lease.get_session_lease()
if not session:
return None
worktree = (session.get("worktree") or "").strip()
return worktree or None
def _resolve_reviewer_mutation_worktree_path(
worktree_path: str | None = None,
worktree: str | None = None,
) -> str:
"""Resolve the workspace inspected before reviewer mutations (#503)."""
return reviewer_mutation_workspace.resolve_reviewer_mutation_workspace(
worktree_path=worktree_path,
worktree=worktree,
process_project_root=PROJECT_ROOT,
active_worktree_env=os.environ.get(ACTIVE_WORKTREE_ENV),
session_lease_worktree=_reviewer_session_worktree(),
)
def _assess_reviewer_mutation_workspace(
worktree_path: str | None = None,
worktree: str | None = None,
) -> dict:
git_state = issue_lock_worktree.read_worktree_git_state(
_resolve_reviewer_mutation_worktree_path(worktree_path, worktree)
)
return reviewer_mutation_workspace.assess_reviewer_mutation_workspace(
worktree_path=worktree_path,
worktree=worktree,
process_project_root=PROJECT_ROOT,
active_worktree_env=os.environ.get(ACTIVE_WORKTREE_ENV),
session_lease_worktree=_reviewer_session_worktree(),
current_branch=git_state.get("current_branch"),
)
def _verify_reviewer_mutation_workspace(
remote: str | None = None,
worktree_path: str | None = None,
worktree: str | None = None,
) -> str:
"""Bind reviewer mutations to the active branches/ worktree (#503)."""
assessment = _assess_reviewer_mutation_workspace(worktree_path, worktree)
if assessment["block"]:
raise RuntimeError(
reviewer_mutation_workspace.format_stale_mcp_workspace_binding_error(
process_project_root=assessment["process_project_root"],
mutation_workspace=assessment["mutation_workspace"],
declared_worktree_path=assessment.get("declared_worktree_path"),
metadata_only=assessment.get("metadata_only", False),
)
+ (
f" Details: {'; '.join(assessment['reasons'])}"
if assessment.get("reasons")
else ""
)
)
resolved = assessment["mutation_workspace"]
verify_preflight_purity(remote, worktree_path=resolved)
return resolved
def _get_git_root(path: str) -> str | None:
try:
res = subprocess.run(
@@ -332,6 +397,22 @@ def assess_preflight_status(worktree_path: str | None = None) -> dict:
"Active task workspace has tracked file edits before mutation "
f"({_format_preflight_workspace_details(workspace_details)})"
)
if _preflight_resolved_role == "reviewer":
binding = reviewer_mutation_workspace.assess_metadata_only_worktree_binding(
declared_worktree_path=worktree_path,
mutation_workspace=_resolve_reviewer_mutation_worktree_path(),
process_project_root=PROJECT_ROOT,
)
if binding.get("block"):
reasons.append(binding["reasons"][0])
reasons.append(
reviewer_mutation_workspace.format_stale_mcp_workspace_binding_error(
process_project_root=PROJECT_ROOT,
mutation_workspace=binding["mutation_workspace"],
declared_worktree_path=binding.get("declared_worktree_path"),
metadata_only=True,
)
)
return {
"preflight_ready": not reasons,
"preflight_block_reasons": reasons,
@@ -542,6 +623,7 @@ import author_mutation_worktree # noqa: E402
import issue_claim_heartbeat # noqa: E402
import issue_work_duplicate_gate # noqa: E402
import reviewer_pr_lease # noqa: E402
import reviewer_mutation_workspace # noqa: E402
import merged_cleanup_reconcile # noqa: E402
import reconciler_profile # noqa: E402
import reconciliation_workflow # noqa: E402
@@ -2639,9 +2721,28 @@ def _evaluate_pr_review_submission(
*,
live: bool,
final_review_decision_ready: bool = False,
worktree_path: str | None = None,
) -> dict:
"""Shared gate chain for live submit and dry-run review tools."""
verify_preflight_purity(remote)
try:
_verify_reviewer_mutation_workspace(
remote, worktree_path=worktree_path
)
except RuntimeError as exc:
return {
"requested_action": (action or "").strip().lower(),
"performed": False,
"dry_run": not live,
"would_perform": False,
"authenticated_user": None,
"profile_name": get_profile()["profile_name"],
"pr_author": None,
"pr_number": pr_number,
"head_sha": None,
"expected_head_sha": expected_head_sha,
"remote": remote if remote in REMOTES else None,
"reasons": [str(exc)],
}
action = (action or "").strip().lower()
result = {
"requested_action": action,
@@ -2832,9 +2933,16 @@ def gitea_mark_final_review_decision(
remote: str = "dadeschools",
org: str | None = None,
repo: str | None = None,
worktree_path: str | None = None,
) -> dict:
"""Mark validation complete; the final review decision is ready to submit."""
action = (action or "").strip().lower()
try:
_verify_reviewer_mutation_workspace(
remote, worktree_path=worktree_path
)
except RuntimeError as exc:
return {"marked_ready": False, "reasons": [str(exc)]}
lock = _load_review_decision_lock()
if lock is None:
return {
@@ -3041,6 +3149,7 @@ def gitea_dry_run_pr_review(
host: str | None = None,
org: str | None = None,
repo: str | None = None,
worktree_path: str | None = None,
) -> dict:
"""Validate review submission mechanics without a live PR mutation."""
return _evaluate_pr_review_submission(
@@ -3053,6 +3162,7 @@ def gitea_dry_run_pr_review(
org=org,
repo=repo,
live=False,
worktree_path=worktree_path,
)
@@ -3068,6 +3178,7 @@ def gitea_submit_pr_review(
org: str | None = None,
repo: str | None = None,
final_review_decision_ready: bool = False,
worktree_path: str | None = None,
) -> dict:
"""Gated PR review mutation: comment findings, request changes, or approve.
@@ -3086,6 +3197,7 @@ def gitea_submit_pr_review(
repo=repo,
live=True,
final_review_decision_ready=final_review_decision_ready,
worktree_path=worktree_path,
)
@@ -4982,7 +5094,17 @@ def gitea_acquire_reviewer_pr_lease(
"permission_report": _permission_block_report("gitea.pr.comment"),
}
verify_preflight_purity(remote)
try:
resolved_worktree = _verify_reviewer_mutation_workspace(
remote, worktree=worktree
)
except RuntimeError as exc:
return {
"success": False,
"acquired": False,
"reasons": [str(exc)],
"permission_report": None,
}
h, o, r = _resolve(remote, host, org, repo)
auth = _auth(h)
profile = get_profile()
@@ -5000,7 +5122,7 @@ def gitea_acquire_reviewer_pr_lease(
session_id=sid,
repo=repo_label,
issue_number=issue_number,
worktree=worktree,
worktree=resolved_worktree,
candidate_head=candidate_head,
target_branch=target_branch,
target_branch_sha=target_branch_sha,
@@ -5032,7 +5154,7 @@ def gitea_acquire_reviewer_pr_lease(
"session_id": sid,
"reviewer_identity": identity,
"profile": profile.get("profile_name"),
"worktree": worktree,
"worktree": resolved_worktree,
"phase": "claimed",
"candidate_head": candidate_head,
"target_branch": target_branch,
@@ -5083,7 +5205,14 @@ def gitea_heartbeat_reviewer_pr_lease(
],
}
verify_preflight_purity(remote)
try:
_verify_reviewer_mutation_workspace(
remote,
worktree=worktree,
worktree_path=session.get("worktree"),
)
except RuntimeError as exc:
return {"success": False, "posted": False, "reasons": [str(exc)]}
h, o, r = _resolve(remote, host, org, repo)
auth = _auth(h)
body = reviewer_pr_lease.format_lease_body(