feat: add conflict-fix leases and stale-head protection (Closes #399)

Introduce structured PR work leases so author conflict-fix pushes cannot
race reviewer validation, approval, or merge on a moving head SHA.

- pr_work_lease.py: parse/acquire leases, push and reviewer mutation gates
- gitea_acquire_conflict_fix_lease, gitea_assess_conflict_fix_push MCP tools
- Enforce reviewed head SHA on mark_final_review_decision, submit_pr_review, merge_pr
- Final-report rules and workflow sections 20A / 26B
- tests/test_pr_work_lease.py (14 cases)
This commit is contained in:
2026-07-07 13:06:07 -04:00
parent 1a1e679246
commit 87beb44394
6 changed files with 993 additions and 1 deletions
+41
View File
@@ -490,6 +490,45 @@ def _rule_reviewer_validation_failure_history(
]
def _rule_reviewer_stale_head_proof(report_text: str) -> list[dict[str, str]]:
from pr_work_lease import assess_reviewer_stale_head_final_report
result = assess_reviewer_stale_head_final_report(report_text)
if result.get("proven"):
return []
return _findings_from_reasons(
"reviewer.stale_head_proof",
result.get("reasons") or [],
field="Stale-head proof",
severity="block",
safe_next_action=(
"state reviewed head SHA, live head before approval/merge, and "
"whether any push occurred during validation"
),
)
def _rule_conflict_fix_push_proof(report_text: str) -> list[dict[str, str]]:
from pr_work_lease import assess_conflict_fix_final_report
text = report_text or ""
if "conflict-fix" not in text.lower() and "conflict fix" not in text.lower():
return []
result = assess_conflict_fix_final_report(text)
if result.get("proven"):
return []
return _findings_from_reasons(
"author.conflict_fix_push_proof",
result.get("reasons") or [],
field="Conflict-fix push proof",
severity="block",
safe_next_action=(
"state branch head before/after push, reviewer lease status, "
"fast-forward status, and whether any reviewer was active"
),
)
def _rule_reviewer_validation_command(report_text: str) -> list[dict[str, str]]:
text = report_text or ""
if not _BARE_PYTEST_RE.search(text):
@@ -909,6 +948,7 @@ _RULES_BY_TASK: dict[str, list[Callable[..., list[dict[str, str]]]]] = {
_rule_reviewer_target_branch_freshness,
_rule_reviewer_mutation_ledger,
_rule_reviewer_review_mutation,
_rule_reviewer_stale_head_proof,
],
"reconcile_already_landed": [
_rule_reconcile_controller_handoff,
@@ -930,6 +970,7 @@ _RULES_BY_TASK: dict[str, list[Callable[..., list[dict[str, str]]]]] = {
_rule_shared_controller_handoff,
_rule_shared_email_disclosure,
_rule_reviewer_vague_mutations_none,
_rule_conflict_fix_push_proof,
],
"issue_filing": [
_rule_shared_controller_handoff,