Remediate PR #861 findings F1-F9 and TestWorktreeStart failures (#860)

- Fix F1: Prepare recovery worktree detached at remote_head without git checkout -B to avoid exit 128 when branch is held by source worktree
- Fix F2: Pass recovery_sanctioned=True in bind_session_lock and assess_same_issue_lease_conflict
- Fix F3: Add SOURCE_RECOVER_DIRTY_ORPHANED to SANCTIONED_LOCK_SOURCES
- Fix F4: Stop after Phase 4 dirty apply when conflicts exist; do not finalize session binding
- Fix F5: Dynamically query competing live locks and workflow leases in MCP server
- Fix F6: Fail closed on recovery worktree resume when HEAD does not match expected remote_head
- Fix F7: Fail closed on remote HEAD observation failure rather than copying expected_remote_head pin
- Fix F8: Enforce foreign overwrite protection requiring same claimant or sanctioned reclaim
- Fix F9: Add real multi-worktree integration tests for prepare_recovery_worktree and lock rebind
- Fix TestWorktreeStart: Bypass session lock check for dry-run and review/pr-* branches in scripts/worktree-start
This commit is contained in:
2026-07-23 22:34:35 -05:00
parent 18d6583e83
commit 0b60fd6557
7 changed files with 200 additions and 40 deletions
+32 -21
View File
@@ -725,13 +725,14 @@ def prepare_recovery_worktree(
)
head = (probe.stdout or "").strip()
if probe.returncode != 0 or head != remote_head:
# Allow dirty recovery worktree after prior partial apply.
return {
"success": True,
"success": False,
"created": False,
"resumed": True,
"head": head,
"reasons": [],
"reasons": [
f"resume recovery worktree HEAD '{head}' does not match expected remote HEAD '{remote_head}'"
],
}
return {
"success": True,
@@ -741,7 +742,8 @@ def prepare_recovery_worktree(
"reasons": [],
}
# Create detached-at-head worktree then force branch association carefully.
# Create detached-at-head worktree. Do not run git checkout -B because
# the source worktree holds the branch name (Git exit 128).
add = git.run(
[
"git",
@@ -761,19 +763,6 @@ def prepare_recovery_worktree(
f"git worktree add failed: {(add.stderr or add.stdout or '').strip()}"
],
}
# Create/update local branch pointer without moving source.
br = git.run(
["git", "checkout", "-B", branch_name],
cwd=recovery_worktree_path,
)
if br.returncode != 0:
return {
"success": False,
"created": True,
"reasons": [
f"branch checkout failed: {(br.stderr or br.stdout or '').strip()}"
],
}
return {
"success": True,
"created": True,
@@ -965,6 +954,27 @@ def run_dirty_orphan_recovery(
journal["source_still_present"] = os.path.isdir(source_worktree_path)
save_journal(journal, journal_dir=journal_dir)
# #860 F4: Do NOT finalize session binding if conflicts remain.
if conflicts:
return {
"success": False,
"performed": True,
"outcome": CONFLICTS_PRESENT,
"reasons": [
"conflicts present: manual resolution required before session binding (fail closed)"
],
"conflicts": list(conflicts),
"recovery_worktree_path": recovery_worktree_path,
"source_worktree_path": source_worktree_path,
"source_frozen": True,
"journal": journal,
"evidence": {
"written_paths": written,
"conflict_state_path": conflict_state_path,
"accepted_head": expected_remote_head,
},
}
# Phase 5: bind session (optional for pure assessor tests).
pid = session_pid if session_pid is not None else os.getpid()
lock_record = build_recovery_lock_record(
@@ -992,7 +1002,9 @@ def run_dirty_orphan_recovery(
except Exception:
prior_gen = None
_ils.bind_session_lock(
lock_record, expected_generation=prior_gen
lock_record,
expected_generation=prior_gen,
recovery_sanctioned=True,
)
else:
lock_writer(lock_record)
@@ -1013,13 +1025,12 @@ def run_dirty_orphan_recovery(
journal["complete"] = True
save_journal(journal, journal_dir=journal_dir)
outcome = CONFLICTS_PRESENT if conflicts else RECOVERY_COMPLETED
return {
"success": True,
"performed": True,
"outcome": outcome,
"outcome": RECOVERY_COMPLETED,
"reasons": [],
"conflicts": list(conflicts),
"conflicts": [],
"recovery_worktree_path": recovery_worktree_path,
"source_worktree_path": source_worktree_path,
"source_frozen": True,