feat(reviewer): fail closed on dirty worktrees and forbidden git cleanup (#233)

Add reviewer_worktree proofs that block stash/reset/checkout -- cleanup of
unrelated local files, require scratch worktree reporting when the main tree
is dirty outside PR scope, and integrate the gate into final report grading
and Controller Handoff review fields.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
2026-07-06 02:04:34 -04:00
co-authored by Claude Opus 4.8
parent 6ac6b9528c
commit 4f466550ca
5 changed files with 459 additions and 1 deletions
+28 -1
View File
@@ -17,6 +17,7 @@ here weakens or replaces them.
import re
import issue_duplicate_gate
from reviewer_worktree import assess_reviewer_worktree_proof
_FULL_SHA = re.compile(r"^[0-9a-f]{40}$")
@@ -637,7 +638,7 @@ def build_final_report(checkout_proof, inventory, validation, contamination,
role_boundary=None, review_mutation=None,
report_text=None, review_decision_lock=None,
controller_handoff=None, capability_proof=None,
sweep_proof=None):
sweep_proof=None, worktree_proof=None):
"""Required behavior 6 + acceptance criteria: one report, distinct proofs.
Combines the individual proof verdicts into the final-report fields the
@@ -707,6 +708,14 @@ def build_final_report(checkout_proof, inventory, validation, contamination,
"downgraded": True,
"reasons": ["review mutation proof not provided (#211)"],
}
if worktree_proof is not None:
worktree = assess_reviewer_worktree_proof(worktree_proof)
else:
worktree = {
"proven": False,
"block": True,
"reasons": ["reviewer worktree proof not provided (#233)"],
}
capability_proven = bool(capability_evidence.get("proven"))
sweep_proven = bool(sweep.get("proven"))
@@ -719,6 +728,7 @@ def build_final_report(checkout_proof, inventory, validation, contamination,
"reasons": ["review mutation proof missing"],
}
review_mutation_complete = bool(review_mutation.get("complete"))
worktree_proven = bool(worktree.get("proven"))
downgrade_reasons = []
if not identity_eligible:
@@ -772,6 +782,11 @@ def build_final_report(checkout_proof, inventory, validation, contamination,
if not review_mutation_complete:
downgrade_reasons.append("review mutation proof missing or incomplete (#211)")
downgrade_reasons.extend(review_mutation.get("reasons", []))
if not worktree_proven:
downgrade_reasons.append(
"reviewer worktree safety proof missing or failed (#233)"
)
downgrade_reasons.extend(worktree.get("reasons", []))
merge_allowed = (
identity_eligible
@@ -782,6 +797,7 @@ def build_final_report(checkout_proof, inventory, validation, contamination,
and validation.get("verdict") != "invalid"
# #179: no merge without a proven final live-state recheck.
and live_state_proven
and worktree_proven
)
violations = []
@@ -825,6 +841,11 @@ def build_final_report(checkout_proof, inventory, validation, contamination,
"live_state_recheck_proven": live_state_proven,
"role_boundary_clean": role_boundary_clean,
"review_mutation_complete": review_mutation_complete,
"worktree_proof_proven": worktree_proven,
"worktree_scratch_used": bool(worktree.get("scratch_used")),
"unrelated_mutations_avoided": bool(
worktree.get("unrelated_mutations_avoided")
),
}
@@ -971,6 +992,12 @@ HANDOFF_ROLE_FIELDS = {
("Selected PR", ("selected pr",)),
("Reviewer eligibility", ("reviewer eligibility", "eligibility")),
("Pinned reviewed head", ("pinned reviewed head", "pinned head")),
("Worktree path", ("worktree path", "starting worktree path")),
("Worktree dirty", ("worktree dirty", "whether worktree was dirty")),
("Scratch worktree used", ("scratch worktree used", "scratch clone used",
"scratch worktree")),
("Unrelated local mutations", ("unrelated local mutations",
"unrelated files modified")),
("Review decision", ("review decision", "decision")),
("Merge result", ("merge result",)),
("Linked issue status", ("linked issue status", "linked issue")),