Merge branch 'master' into feat/issue-610-live-remote-parity

This commit is contained in:
2026-07-22 04:30:44 -05:00
9 changed files with 1847 additions and 27 deletions
+183 -13
View File
@@ -2017,6 +2017,7 @@ import issue_lock_provenance # noqa: E402
import issue_lock_store # noqa: E402
import issue_lock_adoption # noqa: E402
import issue_lock_recovery # noqa: E402
import issue_lock_renewal # noqa: E402
import stacked_pr_support # noqa: E402
import merge_approval_gate # noqa: E402
import review_quarantine # noqa: E402 # #695 contaminated formal-review quarantine
@@ -2316,7 +2317,12 @@ def _resolve_issue_lock_for_pr(
return lock_data
def _save_issue_lock(data: dict, *, expected_generation: int | None = None) -> str:
def _save_issue_lock(
data: dict,
*,
expected_generation: int | None = None,
renewal_sanctioned: bool = False,
) -> str:
existing = issue_lock_store.load_issue_lock(
remote=str(data.get("remote") or ""),
org=str(data.get("org") or ""),
@@ -2328,7 +2334,9 @@ def _save_issue_lock(data: dict, *, expected_generation: int | None = None) -> s
raise RuntimeError(overwrite_block)
try:
return issue_lock_store.bind_session_lock(
data, expected_generation=expected_generation
data,
expected_generation=expected_generation,
renewal_sanctioned=renewal_sanctioned,
)
except Exception as e:
raise RuntimeError(f"Could not write issue lock file: {e}") from e
@@ -2448,6 +2456,79 @@ def _evaluate_issue_lock_recovery(
)
def _evaluate_issue_lock_renewal(
existing_lock: dict,
*,
issue_number: int,
branch_name: str,
worktree_path: str,
remote: str,
h: str | None,
o: str,
r: str,
git_state: dict,
) -> dict:
"""Gather evidence and decide exact-owner renewal of an expired lease (#760).
Mirrors ``_evaluate_issue_lock_recovery``: every input is durable lock state
or a live server-side observation (Gitea branch/PR inventory, git in the
declared worktree, the local lock store). Nothing is reachable from an MCP
caller's parameters, so no caller can assert its way into a renewal
(#760 AC14).
"""
renewal_auth = _auth(h)
try:
renewal_branches = api_get_all(
f"{repo_api_url(h, o, r)}/branches", renewal_auth
)
except Exception as exc:
raise RuntimeError(
f"Could not list branches to verify exact-owner lease renewal: {exc}"
)
remote_head: str | None = None
candidates: list[str] = []
for entry in renewal_branches:
entry_name = _branch_entry_name(entry)
if entry_name == branch_name:
remote_head = _branch_entry_commit_sha(entry)
if issue_lock_adoption.branch_carries_issue_marker(entry_name, issue_number):
candidates.append(entry_name)
pr_head: str | None = None
pr_number: int | None = None
for pull in _list_open_pulls(h, o, r, renewal_auth):
pull_head = pull.get("head") or {}
if str(pull_head.get("ref") or "") == branch_name:
pr_head = pull_head.get("sha")
pr_number = pull.get("number")
break
claimant = _work_lease_claimant(h)
return issue_lock_renewal.assess_exact_owner_lease_renewal(
existing_lock,
issue_number=issue_number,
branch_name=branch_name,
worktree_path=worktree_path,
remote=remote,
org=o,
repo=r,
identity=claimant.get("username"),
profile=claimant.get("profile"),
operation_type=AUTHOR_ISSUE_WORK_LEASE,
current_branch=git_state.get("current_branch"),
porcelain_status=git_state.get("porcelain_status") or "",
worktree_exists=os.path.isdir(os.path.realpath(worktree_path)),
head_sha=git_state.get("head_sha"),
remote_head_sha=remote_head,
pr_head_sha=pr_head,
pr_number=pr_number,
competing_live_locks=issue_lock_store.list_live_locks(),
candidate_branches=candidates,
current_pid=os.getpid(),
)
def _work_lease_claimant(host: str | None) -> dict:
profile = get_profile()
username = _IDENTITY_CACHE.get(host) if host else None
@@ -3893,15 +3974,12 @@ def gitea_lock_issue(
existing_issue_lock = _load_existing_issue_lock(
remote=remote, org=o, repo=r, issue_number=issue_number
)
active_lease_block = issue_lock_store.assess_same_issue_lease_conflict(
existing_issue_lock,
issue_number=issue_number,
branch_name=branch_name,
worktree_path=resolved_worktree,
operation_type=AUTHOR_ISSUE_WORK_LEASE,
)
if active_lease_block:
raise RuntimeError(active_lease_block)
# #760: the competing-lease disposition is decided below, once the worktree
# and Gitea evidence an exact-owner renewal depends on has actually been
# observed. Deciding it here — before any of that exists — is what made the
# same-owner allowance unreachable for an expired lease. The authoritative
# check still runs inside bind_session_lock under the per-issue flock, so
# moving this one later cannot widen the window for a competing writer.
# ── Stacked-PR base declaration (opt-in, #484) ──
# Normal work leaves stacked_base_branch None → master-equivalent path.
@@ -3967,6 +4045,53 @@ def gitea_lock_issue(
recovery_sanctioned = bool(
recovery_assessment and recovery_assessment.get("recovery_sanctioned")
)
# ── Exact-owner renewal of an expired lease (#760) ──
# The opposite trigger from #753 above: there the lease is unexpired and the
# PID is dead; here the lease has expired while the recording daemon — which
# is the long-lived MCP server, not the authoring task — may well still be
# up. Only an expired lease is assessed, so a live foreign lease is never a
# candidate (AC12) and dead-PID takeover keeps its existing conditions
# (AC11). A refusal never raises: it withholds the waiver and leaves the
# conflict check below to fail closed exactly as before.
renewal_assessment: dict | None = None
if (
existing_issue_lock
and existing_issue_lock.get("issue_number") == issue_number
and issue_lock_store.is_lease_expired(existing_issue_lock)
):
renewal_assessment = _evaluate_issue_lock_renewal(
existing_issue_lock,
issue_number=issue_number,
branch_name=branch_name,
worktree_path=resolved_worktree,
remote=remote,
h=h,
o=o,
r=r,
git_state=git_state,
)
renewal_sanctioned = bool(
renewal_assessment and renewal_assessment.get("renewal_sanctioned")
)
active_lease_block = issue_lock_store.assess_same_issue_lease_conflict(
existing_issue_lock,
issue_number=issue_number,
branch_name=branch_name,
worktree_path=resolved_worktree,
operation_type=AUTHOR_ISSUE_WORK_LEASE,
renewal_sanctioned=renewal_sanctioned,
)
if active_lease_block:
reasons = [active_lease_block]
# Name the exact missing evidence when this looked like a renewal, so a
# blocked owner sees why rather than only the generic takeover text.
if renewal_assessment and renewal_assessment.get("is_candidate"):
reasons.append(
issue_lock_renewal.format_renewal_refusal(renewal_assessment)
)
raise RuntimeError("; ".join(reasons))
# #755: a sanctioned dead-session recovery always has an owning open PR —
# that is what makes it a recovery rather than a fresh claim. Carry the
# server-derived owning-PR evidence into the duplicate-work gate below so
@@ -3978,6 +4103,14 @@ def gitea_lock_issue(
if recovery_sanctioned
else None
)
# #760: a sanctioned renewal owns its open PR for the same reason, so it
# needs the same exemption. Without it the duplicate-work gate rejects every
# renewal with "open PR already covers issue", which is the PR the lock
# being renewed already owns. Withheld unless renewal was granted.
if recovered_owning_pr is None and renewal_sanctioned:
recovered_owning_pr = issue_lock_renewal.owning_pr_renewal_evidence(
renewal_assessment
)
lock_assessment = issue_lock_worktree.assess_issue_lock_worktree(
worktree_path=resolved_worktree,
current_branch=git_state.get("current_branch"),
@@ -3986,6 +4119,10 @@ def gitea_lock_issue(
inspected_git_root=git_state.get("inspected_git_root"),
base_branch=git_state.get("base_branch"),
recovery_sanctioned=recovery_sanctioned,
# #760: without this the renewal waiver was computed and then discarded
# here — the exact-owner branch always carries commits, so it can never
# be base-equivalent, and every real renewal failed at this gate.
renewal_sanctioned=renewal_sanctioned,
)
if lock_assessment["block"]:
reasons = list(lock_assessment.get("reasons") or [])
@@ -3995,6 +4132,12 @@ def gitea_lock_issue(
reasons.append(
issue_lock_recovery.format_recovery_refusal(recovery_assessment)
)
# #760: same courtesy for a refused renewal, so an exact owner blocked
# at this gate sees which piece of ownership evidence was missing.
if renewal_assessment and renewal_assessment.get("is_candidate"):
reasons.append(
issue_lock_renewal.format_renewal_refusal(renewal_assessment)
)
raise RuntimeError(
issue_lock_worktree.format_issue_lock_worktree_error(
{**lock_assessment, "reasons": reasons}
@@ -4070,18 +4213,35 @@ def gitea_lock_issue(
recovery_assessment,
recovered_at=_work_lease_timestamp(_work_lease_now()),
)
if renewal_sanctioned and renewal_assessment:
# #760 AC9: record both sides of the transition — prior PID and expiry,
# replacement PID and new expiry — so a renewed lock is auditable and
# never reads as an original claim.
data["lease_renewal"] = issue_lock_renewal.build_renewal_record(
renewal_assessment,
renewed_at=_work_lease_timestamp(_work_lease_now()),
new_expires_at=str(work_lease.get("expires_at") or ""),
)
# #772 AC5: a recovery replaces a claim another session already owned, so
# its write is a compare-and-swap against the generation the assessment was
# made on. Two replacement sessions that both observed the same dead owner
# cannot both succeed — the second finds a moved generation and fails
# closed. Ordinary first-time claims keep the unconditional write.
# #760 uses the same compare-and-swap: a renewal also replaces a claim that
# already existed on disk, so two sessions that both observed the same
# expired lease cannot both win — the second finds a moved generation and
# fails closed.
expected_generation = (
issue_lock_store.lock_generation(existing_issue_lock)
if recovery_sanctioned
if (recovery_sanctioned or renewal_sanctioned)
else None
)
lock_file_path = _save_issue_lock(data, expected_generation=expected_generation)
lock_file_path = _save_issue_lock(
data,
expected_generation=expected_generation,
renewal_sanctioned=renewal_sanctioned,
)
lock_record = issue_lock_store.read_lock_file(lock_file_path) or data
freshness = issue_lock_store.assess_lock_freshness(lock_record)
competing = [
@@ -4150,6 +4310,16 @@ def gitea_lock_issue(
issue_number=issue_number,
branch_name=branch_name,
)
if renewal_sanctioned:
# #760 AC13: the renewal is visible in the native tool result, so an
# owner never has to inspect the lock file to confirm what happened.
result["lease_renewal"] = lock_record.get("lease_renewal")
result["message"] = (
f"Renewed the expired {AUTHOR_ISSUE_WORK_LEASE} lease on issue "
f"#{issue_number} for its exact recorded owner, on branch "
f"'{branch_name}' from worktree '{resolved_worktree}' "
"(fail-closed check complete)."
)
if agent_artifacts:
result["warnings"] = [
"Agent temp artifacts at repo root (delete before implementation): "