Make active leases queryable workflow state with canonical adopt, release, expire/reclaim, and abandon operations on the #613 control-plane DB substrate. - lease_lifecycle.py: policy, proof gates, safe_next_action vocabulary - control_plane_db: schema v3 provenance columns + list/inspect/adopt/abandon - MCP tools: gitea_*_workflow_lease(s) for list/inspect/adopt/release/expire/abandon/reclaim - issue_lock_store: sanctioned reclaim of expired author locks (dead pid / missing wt) - Tests cover lifecycle, foreign steal refusal, allocator compatibility, merger handoff
675 lines
21 KiB
Python
675 lines
21 KiB
Python
"""Keyed, persistent issue-lock storage (#443) with flock hardening (#438).
|
|
|
|
Replaces the single global ``/tmp/gitea_issue_lock.json`` slot with per-issue
|
|
lock files under ``GITEA_ISSUE_LOCK_DIR`` (default
|
|
``~/.cache/gitea-tools/issue-locks``). Each MCP session binds its active lock
|
|
via a per-process pointer file so concurrent repos/issues never clobber each
|
|
other. Acquisition is serialized per issue with ``fcntl.flock``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import errno
|
|
import fcntl
|
|
import json
|
|
import os
|
|
import re
|
|
import tempfile
|
|
from contextlib import contextmanager
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import Any
|
|
|
|
LOCK_DIR_ENV = "GITEA_ISSUE_LOCK_DIR"
|
|
DEFAULT_LOCK_DIR = os.path.expanduser("~/.cache/gitea-tools/issue-locks")
|
|
WORK_LEASE_TTL_HOURS = 4
|
|
AUTHOR_ISSUE_WORK_LEASE = "author_issue_work"
|
|
|
|
_SAFE_SEGMENT_RE = re.compile(r"[^A-Za-z0-9._+-]+")
|
|
|
|
|
|
class LockContentionError(RuntimeError):
|
|
"""Raised when an exclusive per-issue lock cannot be acquired."""
|
|
|
|
|
|
def default_lock_dir() -> str:
|
|
raw = (os.environ.get(LOCK_DIR_ENV) or DEFAULT_LOCK_DIR).strip()
|
|
return raw or DEFAULT_LOCK_DIR
|
|
|
|
|
|
def _sanitize_segment(value: str) -> str:
|
|
text = (value or "").strip()
|
|
if not text:
|
|
return "_"
|
|
return _SAFE_SEGMENT_RE.sub("_", text)
|
|
|
|
|
|
def lock_key(
|
|
*,
|
|
remote: str,
|
|
org: str,
|
|
repo: str,
|
|
issue_number: int,
|
|
) -> str:
|
|
return "-".join(
|
|
_sanitize_segment(part)
|
|
for part in (remote, org, repo, str(issue_number))
|
|
)
|
|
|
|
|
|
def lock_file_path(
|
|
*,
|
|
remote: str,
|
|
org: str,
|
|
repo: str,
|
|
issue_number: int,
|
|
lock_dir: str | None = None,
|
|
) -> str:
|
|
root = (lock_dir or default_lock_dir()).strip()
|
|
return os.path.join(root, f"{lock_key(remote=remote, org=org, repo=repo, issue_number=issue_number)}.json")
|
|
|
|
|
|
def session_pointer_path(lock_dir: str | None = None) -> str:
|
|
root = (lock_dir or default_lock_dir()).strip()
|
|
return os.path.join(root, f"session-{os.getpid()}.json")
|
|
|
|
|
|
def _ensure_lock_dir(lock_dir: str | None = None) -> str:
|
|
root = (lock_dir or default_lock_dir()).strip()
|
|
os.makedirs(root, mode=0o700, exist_ok=True)
|
|
return root
|
|
|
|
|
|
def flock_path(json_path: str) -> str:
|
|
return f"{json_path}.lock"
|
|
|
|
|
|
def is_process_alive(pid: int | None) -> bool:
|
|
if not pid or pid <= 0:
|
|
return False
|
|
try:
|
|
os.kill(int(pid), 0)
|
|
return True
|
|
except OSError as exc:
|
|
return exc.errno != errno.ESRCH
|
|
except (TypeError, ValueError):
|
|
return False
|
|
|
|
|
|
@contextmanager
|
|
def _exclusive_file_lock(lock_path: str):
|
|
os.makedirs(os.path.dirname(lock_path) or ".", exist_ok=True)
|
|
fd = os.open(lock_path, os.O_CREAT | os.O_RDWR, 0o600)
|
|
try:
|
|
try:
|
|
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
except BlockingIOError as exc:
|
|
raise LockContentionError(
|
|
f"could not acquire exclusive lock on '{lock_path}'"
|
|
) from exc
|
|
yield fd
|
|
finally:
|
|
try:
|
|
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
finally:
|
|
os.close(fd)
|
|
|
|
|
|
def read_lock_file(path: str) -> dict[str, Any] | None:
|
|
lock_path = (path or "").strip()
|
|
if not lock_path or not os.path.exists(lock_path):
|
|
return None
|
|
try:
|
|
with open(lock_path, encoding="utf-8") as handle:
|
|
data = json.load(handle)
|
|
except (OSError, json.JSONDecodeError):
|
|
return None
|
|
return data if isinstance(data, dict) else None
|
|
|
|
|
|
def save_lock_file(path: str, data: dict[str, Any]) -> None:
|
|
lock_path = (path or "").strip()
|
|
if not lock_path:
|
|
raise ValueError("lock path is required (fail closed)")
|
|
parent = os.path.dirname(lock_path) or "."
|
|
os.makedirs(parent, mode=0o700, exist_ok=True)
|
|
payload = json.dumps(data, indent=2, sort_keys=True) + "\n"
|
|
fd, temp_path = tempfile.mkstemp(prefix=".lock-", suffix=".json", dir=parent)
|
|
try:
|
|
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
handle.write(payload)
|
|
handle.flush()
|
|
os.fsync(handle.fileno())
|
|
os.replace(temp_path, lock_path)
|
|
finally:
|
|
if os.path.exists(temp_path):
|
|
try:
|
|
os.remove(temp_path)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def bind_session_lock(lock_data: dict[str, Any], lock_dir: str | None = None) -> str:
|
|
"""Persist a keyed lock and bind it to the current process session."""
|
|
remote = str(lock_data.get("remote") or "")
|
|
org = str(lock_data.get("org") or "")
|
|
repo = str(lock_data.get("repo") or "")
|
|
issue_number = int(lock_data.get("issue_number") or 0)
|
|
if not remote or not org or not repo or issue_number <= 0:
|
|
raise ValueError("lock record must include remote, org, repo, and issue_number")
|
|
|
|
root = _ensure_lock_dir(lock_dir)
|
|
path = lock_file_path(
|
|
remote=remote,
|
|
org=org,
|
|
repo=repo,
|
|
issue_number=issue_number,
|
|
lock_dir=root,
|
|
)
|
|
record = dict(lock_data)
|
|
record["lock_file_path"] = path
|
|
record["session_pid"] = os.getpid()
|
|
record.setdefault("pid", os.getpid())
|
|
|
|
pointer = {
|
|
"pid": os.getpid(),
|
|
"lock_file_path": path,
|
|
"issue_number": issue_number,
|
|
"branch_name": record.get("branch_name"),
|
|
"remote": remote,
|
|
"org": org,
|
|
"repo": repo,
|
|
}
|
|
sentinel = flock_path(path)
|
|
try:
|
|
with _exclusive_file_lock(sentinel):
|
|
existing = read_lock_file(path)
|
|
overwrite_block = assess_foreign_lock_overwrite(existing, record)
|
|
if overwrite_block:
|
|
raise RuntimeError(overwrite_block)
|
|
lease_block = assess_same_issue_lease_conflict(
|
|
existing,
|
|
issue_number=issue_number,
|
|
branch_name=str(record.get("branch_name") or ""),
|
|
worktree_path=str(record.get("worktree_path") or ""),
|
|
)
|
|
if lease_block:
|
|
raise RuntimeError(lease_block)
|
|
save_lock_file(path, record)
|
|
save_lock_file(session_pointer_path(root), pointer)
|
|
except LockContentionError as exc:
|
|
competing = read_lock_file(path)
|
|
if competing:
|
|
owner_pid = competing.get("session_pid") or competing.get("pid")
|
|
raise RuntimeError(
|
|
f"Issue #{issue_number} lock contention: {exc}; competing owner "
|
|
f"pid={owner_pid} (fail closed)"
|
|
) from exc
|
|
raise RuntimeError(f"Issue #{issue_number} lock contention: {exc} (fail closed)") from exc
|
|
return path
|
|
|
|
|
|
def read_session_issue_lock(lock_dir: str | None = None) -> dict[str, Any] | None:
|
|
root = (lock_dir or default_lock_dir()).strip()
|
|
pointer = read_lock_file(session_pointer_path(root))
|
|
if not pointer:
|
|
return None
|
|
lock_path = str(pointer.get("lock_file_path") or "").strip()
|
|
if not lock_path:
|
|
return None
|
|
return read_lock_file(lock_path)
|
|
|
|
|
|
def load_issue_lock(
|
|
*,
|
|
remote: str,
|
|
org: str,
|
|
repo: str,
|
|
issue_number: int,
|
|
lock_dir: str | None = None,
|
|
) -> dict[str, Any] | None:
|
|
return read_lock_file(
|
|
lock_file_path(
|
|
remote=remote,
|
|
org=org,
|
|
repo=repo,
|
|
issue_number=issue_number,
|
|
lock_dir=lock_dir,
|
|
)
|
|
)
|
|
|
|
|
|
def iter_lock_files(lock_dir: str | None = None) -> list[str]:
|
|
root = (lock_dir or default_lock_dir()).strip()
|
|
if not os.path.isdir(root):
|
|
return []
|
|
paths: list[str] = []
|
|
for name in os.listdir(root):
|
|
if not name.endswith(".json") or name.startswith("session-"):
|
|
continue
|
|
paths.append(os.path.join(root, name))
|
|
return sorted(paths)
|
|
|
|
|
|
def find_lock_for_branch(
|
|
*,
|
|
remote: str,
|
|
org: str,
|
|
repo: str,
|
|
branch_name: str,
|
|
lock_dir: str | None = None,
|
|
) -> dict[str, Any] | None:
|
|
target = (branch_name or "").strip()
|
|
if not target:
|
|
return None
|
|
for path in iter_lock_files(lock_dir):
|
|
lock = read_lock_file(path)
|
|
if not lock:
|
|
continue
|
|
if (
|
|
str(lock.get("remote") or "") == remote
|
|
and str(lock.get("org") or "") == org
|
|
and str(lock.get("repo") or "") == repo
|
|
and str(lock.get("branch_name") or "").strip() == target
|
|
):
|
|
lock = dict(lock)
|
|
lock.setdefault("lock_file_path", path)
|
|
return lock
|
|
return None
|
|
|
|
|
|
def _lease_now(now: datetime | None = None) -> datetime:
|
|
return now or datetime.now(timezone.utc)
|
|
|
|
|
|
def _parse_lease_timestamp(value: str | None) -> datetime | None:
|
|
text = (value or "").strip()
|
|
if not text:
|
|
return None
|
|
try:
|
|
return datetime.fromisoformat(text.replace("Z", "+00:00")).astimezone(timezone.utc)
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def lease_expires_at(lock: dict[str, Any] | None) -> datetime | None:
|
|
if not lock:
|
|
return None
|
|
lease = lock.get("work_lease")
|
|
if not isinstance(lease, dict):
|
|
return None
|
|
return _parse_lease_timestamp(lease.get("expires_at"))
|
|
|
|
|
|
def is_lease_expired(lock: dict[str, Any] | None, *, now: datetime | None = None) -> bool:
|
|
expires = lease_expires_at(lock)
|
|
if expires is None:
|
|
return False
|
|
return expires <= _lease_now(now)
|
|
|
|
|
|
def is_lease_live(lock: dict[str, Any] | None, *, now: datetime | None = None) -> bool:
|
|
return assess_lock_freshness(lock, now=now)["live"]
|
|
|
|
|
|
def assess_lock_freshness(
|
|
lock_data: dict[str, Any] | None,
|
|
*,
|
|
now: datetime | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Classify a lock as live, expired, stale, or absent."""
|
|
current = _lease_now(now)
|
|
if not lock_data:
|
|
return {
|
|
"status": "absent",
|
|
"live": False,
|
|
"stale": False,
|
|
"reason": "no lock record",
|
|
}
|
|
|
|
expires_at = lease_expires_at(lock_data)
|
|
lease = lock_data.get("work_lease")
|
|
heartbeat_at = _parse_lease_timestamp(lock_data.get("last_heartbeat_at"))
|
|
if heartbeat_at is None and isinstance(lease, dict):
|
|
heartbeat_at = _parse_lease_timestamp(lease.get("last_heartbeat_at"))
|
|
|
|
pid = lock_data.get("session_pid")
|
|
if pid is None:
|
|
pid = lock_data.get("pid")
|
|
pid_alive = is_process_alive(pid) if pid is not None else False
|
|
|
|
if expires_at and expires_at <= current:
|
|
return {
|
|
"status": "expired",
|
|
"live": False,
|
|
"stale": True,
|
|
"reason": f"lease expired at {expires_at.isoformat()}",
|
|
"pid_alive": pid_alive,
|
|
}
|
|
|
|
if pid is not None and not pid_alive:
|
|
return {
|
|
"status": "stale",
|
|
"live": False,
|
|
"stale": True,
|
|
"reason": f"owner pid {pid} is not alive",
|
|
"pid_alive": False,
|
|
}
|
|
|
|
return {
|
|
"status": "live",
|
|
"live": True,
|
|
"stale": False,
|
|
"reason": "lock heartbeat and lease are fresh",
|
|
"pid_alive": pid_alive,
|
|
"heartbeat_at": heartbeat_at.isoformat() if heartbeat_at else None,
|
|
"expires_at": expires_at.isoformat() if expires_at else None,
|
|
}
|
|
|
|
|
|
def _same_realpath(left: str | None, right: str | None) -> bool:
|
|
if not left or not right:
|
|
return False
|
|
try:
|
|
return os.path.realpath(left) == os.path.realpath(right)
|
|
except OSError:
|
|
return left == right
|
|
|
|
|
|
|
|
def assess_expired_lock_reclaim(
|
|
existing_lock: dict[str, Any] | None,
|
|
*,
|
|
now: datetime | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Decide whether an expired/stale author issue lock may be reclaimed (#601).
|
|
|
|
Required proof (any reclaim of non-live lock):
|
|
* lease not live (expired or dead pid)
|
|
* owner process dead OR worktree missing
|
|
* no force-delete of live foreign ownership
|
|
"""
|
|
if not existing_lock:
|
|
return {
|
|
"reclaim_allowed": True,
|
|
"reasons": ["no existing lock"],
|
|
"freshness": assess_lock_freshness(None, now=now),
|
|
}
|
|
freshness = assess_lock_freshness(existing_lock, now=now)
|
|
if freshness.get("live"):
|
|
return {
|
|
"reclaim_allowed": False,
|
|
"reasons": ["lock is still live; cannot reclaim (fail closed)"],
|
|
"freshness": freshness,
|
|
}
|
|
pid = existing_lock.get("session_pid")
|
|
if pid is None:
|
|
pid = existing_lock.get("pid")
|
|
dead = not is_process_alive(pid) if pid is not None else True
|
|
wt = str(existing_lock.get("worktree_path") or "")
|
|
missing_wt = (not wt) or (not os.path.isdir(os.path.realpath(wt)))
|
|
if not (dead or missing_wt):
|
|
return {
|
|
"reclaim_allowed": False,
|
|
"reasons": [
|
|
"expired/stale lock still has live owner pid and present worktree; "
|
|
"recovery review required (fail closed)"
|
|
],
|
|
"freshness": freshness,
|
|
"owner_pid_dead": dead,
|
|
"worktree_missing": missing_wt,
|
|
}
|
|
return {
|
|
"reclaim_allowed": True,
|
|
"reasons": [
|
|
"non-live lock with dead process and/or missing worktree; "
|
|
"sanctioned reclaim allowed"
|
|
],
|
|
"freshness": freshness,
|
|
"owner_pid_dead": dead,
|
|
"worktree_missing": missing_wt,
|
|
"prior_branch": existing_lock.get("branch_name"),
|
|
"prior_worktree": existing_lock.get("worktree_path"),
|
|
"prior_pid": pid,
|
|
}
|
|
|
|
|
|
def assess_same_issue_lease_conflict(
|
|
existing_lock: dict[str, Any] | None,
|
|
*,
|
|
issue_number: int,
|
|
branch_name: str,
|
|
worktree_path: str,
|
|
operation_type: str = AUTHOR_ISSUE_WORK_LEASE,
|
|
now: datetime | None = None,
|
|
) -> str | None:
|
|
"""Return a fail-closed error when a competing live lease blocks acquisition."""
|
|
if not existing_lock:
|
|
return None
|
|
|
|
existing_issue = existing_lock.get("issue_number")
|
|
lease = existing_lock.get("work_lease")
|
|
existing_operation = (
|
|
lease.get("operation_type")
|
|
if isinstance(lease, dict)
|
|
else AUTHOR_ISSUE_WORK_LEASE
|
|
)
|
|
if existing_issue != issue_number or existing_operation != operation_type:
|
|
return None
|
|
|
|
existing_branch = existing_lock.get("branch_name")
|
|
existing_worktree = existing_lock.get("worktree_path")
|
|
same_owner = (
|
|
existing_branch == branch_name
|
|
and _same_realpath(str(existing_worktree or ""), worktree_path)
|
|
)
|
|
if is_lease_expired(existing_lock, now=now):
|
|
reclaim = assess_expired_lock_reclaim(existing_lock, now=now)
|
|
if reclaim.get("reclaim_allowed"):
|
|
# #601: expired + dead pid / missing worktree may be reclaimed
|
|
# through the normal lock path (sanctioned overwrite).
|
|
return None
|
|
return (
|
|
f"Issue #{issue_number} has an expired {operation_type} lease on "
|
|
f"branch '{existing_branch}' from worktree '{existing_worktree}'. "
|
|
"Recovery review is required before takeover (fail closed)"
|
|
)
|
|
if same_owner:
|
|
return None
|
|
return (
|
|
f"Issue #{issue_number} already has an active {operation_type} lease on "
|
|
f"branch '{existing_branch}' from worktree '{existing_worktree}' "
|
|
"(fail closed)"
|
|
)
|
|
|
|
|
|
def assess_foreign_lock_overwrite(
|
|
existing_lock: dict[str, Any] | None,
|
|
incoming_lock: dict[str, Any],
|
|
*,
|
|
now: datetime | None = None,
|
|
) -> str | None:
|
|
"""Block writes that would clobber an unrelated live lease on the same key."""
|
|
if not existing_lock:
|
|
return None
|
|
|
|
same_issue = existing_lock.get("issue_number") == incoming_lock.get("issue_number")
|
|
same_branch = existing_lock.get("branch_name") == incoming_lock.get("branch_name")
|
|
same_worktree = _same_realpath(
|
|
str(existing_lock.get("worktree_path") or ""),
|
|
str(incoming_lock.get("worktree_path") or ""),
|
|
)
|
|
if same_issue and same_branch and same_worktree:
|
|
return None
|
|
if not is_lease_live(existing_lock, now=now):
|
|
return None
|
|
return (
|
|
"Refusing to overwrite a live foreign issue lock "
|
|
f"(issue #{existing_lock.get('issue_number')}, "
|
|
f"branch '{existing_lock.get('branch_name')}', "
|
|
f"worktree '{existing_lock.get('worktree_path')}') (fail closed)"
|
|
)
|
|
|
|
|
|
def find_live_lock_for_branch(
|
|
branch_name: str,
|
|
lock_dir: str | None = None,
|
|
) -> dict[str, Any] | None:
|
|
target = (branch_name or "").strip()
|
|
if not target:
|
|
return None
|
|
for path in iter_lock_files(lock_dir):
|
|
lock = read_lock_file(path)
|
|
if not lock:
|
|
continue
|
|
if str(lock.get("branch_name") or "").strip() != target:
|
|
continue
|
|
if not is_lease_live(lock):
|
|
continue
|
|
record = dict(lock)
|
|
record.setdefault("lock_file_path", path)
|
|
return record
|
|
return None
|
|
|
|
|
|
def resolve_locked_branch_for_session(
|
|
branch_name: str | None = None,
|
|
lock_dir: str | None = None,
|
|
) -> str:
|
|
if branch_name:
|
|
lock = find_live_lock_for_branch(branch_name, lock_dir)
|
|
if lock:
|
|
return str(lock.get("branch_name") or "")
|
|
lock = read_session_issue_lock(lock_dir)
|
|
return str((lock or {}).get("branch_name") or "")
|
|
|
|
|
|
def has_active_issue_lock(
|
|
branch: str,
|
|
*,
|
|
lock_dir: str | None = None,
|
|
) -> bool:
|
|
target = (branch or "").strip()
|
|
if not target:
|
|
return False
|
|
for path in iter_lock_files(lock_dir):
|
|
lock = read_lock_file(path)
|
|
if not lock:
|
|
continue
|
|
if str(lock.get("branch_name") or "").strip() != target:
|
|
continue
|
|
if is_lease_live(lock):
|
|
return True
|
|
return False
|
|
|
|
|
|
def verify_lock_for_mutation(
|
|
lock_data: dict[str, Any] | None,
|
|
*,
|
|
issue_number: int | None = None,
|
|
branch_name: str | None = None,
|
|
worktree_path: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Re-check lock ownership immediately before a mutation (#438)."""
|
|
reasons: list[str] = []
|
|
if not lock_data:
|
|
return {"proven": False, "block": True, "reasons": ["issue lock is missing (fail closed)"]}
|
|
|
|
freshness = assess_lock_freshness(lock_data)
|
|
if not freshness["live"]:
|
|
reasons.append(f"issue lock is not live: {freshness['reason']} (fail closed)")
|
|
|
|
if issue_number is not None and lock_data.get("issue_number") != issue_number:
|
|
reasons.append(
|
|
f"issue lock targets #{lock_data.get('issue_number')}, expected #{issue_number} (fail closed)"
|
|
)
|
|
|
|
if branch_name is not None and lock_data.get("branch_name") != branch_name:
|
|
reasons.append(
|
|
f"issue lock branch '{lock_data.get('branch_name')}' does not match "
|
|
f"'{branch_name}' (fail closed)"
|
|
)
|
|
|
|
if worktree_path is not None:
|
|
locked = os.path.realpath(str(lock_data.get("worktree_path") or ""))
|
|
declared = os.path.realpath(worktree_path)
|
|
if locked != declared:
|
|
reasons.append(
|
|
f"issue lock worktree '{locked}' does not match declared '{declared}' (fail closed)"
|
|
)
|
|
|
|
return {
|
|
"proven": not reasons,
|
|
"block": bool(reasons),
|
|
"reasons": reasons,
|
|
"freshness": freshness,
|
|
"lock_proof": format_lock_proof(lock_data, freshness=freshness),
|
|
}
|
|
|
|
|
|
def list_live_locks(
|
|
*,
|
|
lock_dir: str | None = None,
|
|
now: datetime | None = None,
|
|
) -> list[dict[str, Any]]:
|
|
"""Return live per-issue locks for queue visibility."""
|
|
live: list[dict[str, Any]] = []
|
|
for path in iter_lock_files(lock_dir):
|
|
record = read_lock_file(path)
|
|
if not record:
|
|
continue
|
|
freshness = assess_lock_freshness(record, now=now)
|
|
if not freshness["live"]:
|
|
continue
|
|
live.append(
|
|
{
|
|
"issue_number": record.get("issue_number"),
|
|
"branch_name": record.get("branch_name"),
|
|
"remote": record.get("remote"),
|
|
"org": record.get("org"),
|
|
"repo": record.get("repo"),
|
|
"worktree_path": record.get("worktree_path"),
|
|
"pid": record.get("session_pid") or record.get("pid"),
|
|
"claimant": (
|
|
record.get("claimant")
|
|
or (record.get("work_lease") or {}).get("claimant")
|
|
),
|
|
"freshness": freshness,
|
|
"lock_path": record.get("lock_file_path") or path,
|
|
}
|
|
)
|
|
return live
|
|
|
|
|
|
def format_lock_proof(
|
|
lock_data: dict[str, Any] | None,
|
|
*,
|
|
freshness: dict[str, Any] | None = None,
|
|
competing_live_locks: list[dict[str, Any]] | None = None,
|
|
released: bool | None = None,
|
|
) -> str:
|
|
"""Canonical issue-lock proof string for final reports."""
|
|
if not lock_data:
|
|
return "issue lock proof: not acquired"
|
|
fresh = freshness or assess_lock_freshness(lock_data)
|
|
owner = lock_data.get("claimant") or {}
|
|
if not owner and isinstance(lock_data.get("work_lease"), dict):
|
|
owner = lock_data["work_lease"].get("claimant") or {}
|
|
parts = [
|
|
"issue lock proof:",
|
|
f"acquired issue #{lock_data.get('issue_number')}",
|
|
f"branch {lock_data.get('branch_name')}",
|
|
f"owner {owner.get('profile') or 'unknown'}",
|
|
f"pid {lock_data.get('session_pid') or lock_data.get('pid')}",
|
|
f"freshness {fresh.get('status')}",
|
|
]
|
|
if competing_live_locks is not None:
|
|
parts.append(
|
|
"no competing live lock"
|
|
if not competing_live_locks
|
|
else f"competing live locks {len(competing_live_locks)}"
|
|
)
|
|
if released is True:
|
|
parts.append("lock released")
|
|
elif released is False:
|
|
parts.append("lock retained")
|
|
return "; ".join(parts) |