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:
+134
-5
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user