feat: first-class control-plane lease lifecycle (Closes #601)

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
This commit is contained in:
2026-07-10 11:30:53 -04:00
parent a654cda058
commit 780ef0b1be
9 changed files with 2202 additions and 33 deletions
+27 -7
View File
@@ -103,20 +103,40 @@ class TestIssueLockStore(unittest.TestCase):
self.assertIn("live foreign issue lock", block or "")
def test_expired_lease_allows_takeover_with_conflict_check(self):
# #601: expired + dead pid / missing worktree → sanctioned reclaim (no block).
existing = _lock_record(
branch_name="feat/issue-420-other",
worktree_path="/tmp/other",
worktree_path="/tmp/other-does-not-exist-420",
session_pid=1,
work_lease=_lease("2000-01-01T00:00:00Z"),
)
incoming = _lock_record(worktree_path="/tmp/mine")
self.assertIsNone(ils.assess_foreign_lock_overwrite(existing, incoming))
block = ils.assess_same_issue_lease_conflict(
existing,
issue_number=420,
branch_name="feat/issue-420-server-code-parity",
worktree_path="/tmp/mine",
with mock.patch.object(ils, "is_process_alive", return_value=False):
block = ils.assess_same_issue_lease_conflict(
existing,
issue_number=420,
branch_name="feat/issue-420-server-code-parity",
worktree_path="/tmp/mine",
)
self.assertIsNone(block)
# Expired but still-live owner pid AND present worktree → fail closed.
present_wt = self.lock_dir # exists
sticky = _lock_record(
branch_name="feat/issue-420-other",
worktree_path=present_wt,
session_pid=os.getpid(),
work_lease=_lease("2000-01-01T00:00:00Z"),
)
self.assertIn("Recovery review is required", block or "")
with mock.patch.object(ils, "is_process_alive", return_value=True):
blocked = ils.assess_same_issue_lease_conflict(
sticky,
issue_number=420,
branch_name="feat/issue-420-server-code-parity",
worktree_path="/tmp/mine",
)
self.assertIn("Recovery review is required", blocked or "")
def test_same_owner_lease_conflict_allows_refresh(self):
worktree = "/tmp/wt-420"