- 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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user