+435
-387
@@ -302,6 +302,39 @@ def assess_author_issue_bootstrap(
|
||||
}
|
||||
|
||||
|
||||
import fcntl
|
||||
|
||||
|
||||
class BootstrapTransitionLock:
|
||||
"""Inter-process file lock scoped to the transition identity / idempotency key."""
|
||||
|
||||
def __init__(self, idempotency_key: str, journal_dir: str | None = None):
|
||||
safe_key = "".join(
|
||||
c if c.isalnum() or c in ("-", "_", ".") else "_"
|
||||
for c in idempotency_key
|
||||
)
|
||||
lock_dir = get_journal_dir(journal_dir)
|
||||
self.lock_path = os.path.join(lock_dir, f"{safe_key}.lock")
|
||||
self.fd = None
|
||||
|
||||
def __enter__(self):
|
||||
self.fd = open(self.lock_path, "a+")
|
||||
fcntl.flock(self.fd, fcntl.LOCK_EX)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if self.fd:
|
||||
try:
|
||||
fcntl.flock(self.fd, fcntl.LOCK_UN)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.fd.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.fd = None
|
||||
|
||||
|
||||
def bootstrap_author_issue_worktree(
|
||||
*,
|
||||
issue_number: int,
|
||||
@@ -359,418 +392,433 @@ def bootstrap_author_issue_worktree(
|
||||
lease_id=lease_id,
|
||||
)
|
||||
|
||||
# Idempotency check
|
||||
existing = load_phase_journal(key)
|
||||
if existing and existing.get("completed"):
|
||||
if (
|
||||
existing.get("issue_number") == issue_number
|
||||
and existing.get("branch_name") == target_branch
|
||||
and os.path.realpath(existing.get("worktree_path", ""))
|
||||
== target_worktree
|
||||
):
|
||||
# Acquire cross-process file lock scoped to the idempotency key / transition identity
|
||||
with BootstrapTransitionLock(key):
|
||||
# Idempotency check
|
||||
existing = load_phase_journal(key)
|
||||
if existing and existing.get("completed"):
|
||||
if (
|
||||
existing.get("issue_number") == issue_number
|
||||
and existing.get("branch_name") == target_branch
|
||||
and os.path.realpath(existing.get("worktree_path", ""))
|
||||
== target_worktree
|
||||
):
|
||||
return {
|
||||
"success": True,
|
||||
"replayed": True,
|
||||
"message": (
|
||||
f"Idempotent replay: worktree for issue #{issue_number} already bootstrapped at {target_worktree}"
|
||||
),
|
||||
"issue_number": issue_number,
|
||||
"branch_name": target_branch,
|
||||
"worktree_path": target_worktree,
|
||||
"base_sha": existing.get("resolved_base_sha"),
|
||||
"lease_id": existing.get("lease_id"),
|
||||
"assignment_id": existing.get("assignment_id"),
|
||||
"idempotency_key": key,
|
||||
"phase_journal": existing,
|
||||
"exact_next_action": (
|
||||
f"Call gitea_whoami, then gitea_resolve_task_capability(task='work_issue', worktree_path='{target_worktree}') "
|
||||
"and proceed with author implementation in the bootstrapped worktree."
|
||||
),
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"reason_code": "incompatible_idempotency_replay",
|
||||
"message": (
|
||||
f"Idempotency key '{key}' already exists with incompatible parameters "
|
||||
f"(stored: {existing.get('branch_name')}, {existing.get('worktree_path')}; "
|
||||
f"requested: {target_branch}, {target_worktree})"
|
||||
),
|
||||
"exact_next_action": (
|
||||
"Supply a unique idempotency_key or pass compatible parameters."
|
||||
),
|
||||
}
|
||||
|
||||
# Initialize or resume Phase Journal
|
||||
if existing:
|
||||
journal = existing
|
||||
artifacts = journal.setdefault("artifacts_created", {})
|
||||
artifacts.setdefault("branch_created", False)
|
||||
artifacts.setdefault("worktree_dir_created", False)
|
||||
artifacts.setdefault("worktree_registered", False)
|
||||
artifacts.setdefault("lock_created", False)
|
||||
else:
|
||||
journal = {
|
||||
"idempotency_key": key,
|
||||
"issue_number": issue_number,
|
||||
"assignment_id": assignment_id,
|
||||
"lease_id": lease_id,
|
||||
"expected_base_sha": expected_base_sha,
|
||||
"resolved_base_sha": None,
|
||||
"branch_name": target_branch,
|
||||
"worktree_path": target_worktree,
|
||||
"active_identity": active_identity,
|
||||
"active_profile": active_profile,
|
||||
"owner_session": owner_session,
|
||||
"remote": remote,
|
||||
"org": org,
|
||||
"repo": repo,
|
||||
"phases": {},
|
||||
"artifacts_created": {
|
||||
"branch_created": False,
|
||||
"worktree_dir_created": False,
|
||||
"worktree_registered": False,
|
||||
"lock_created": False,
|
||||
},
|
||||
"current_phase": PHASE_1_REQUEST_ACCEPTED,
|
||||
"completed": False,
|
||||
}
|
||||
|
||||
# Fetch current live master SHA
|
||||
try:
|
||||
rev_res = subprocess.run(
|
||||
["git", "-C", root, "rev-parse", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
live_master_sha = rev_res.stdout.strip()
|
||||
except Exception as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"reason_code": "git_rev_parse_failed",
|
||||
"message": f"Could not determine repository HEAD: {exc}",
|
||||
"exact_next_action": "Verify repository git state and retry.",
|
||||
}
|
||||
|
||||
# Phase 1: REQUEST_ACCEPTED & Concurrency Pin Check
|
||||
if expected_base_sha:
|
||||
exp_norm = expected_base_sha.strip().lower()
|
||||
live_norm = live_master_sha.lower()
|
||||
if exp_norm != live_norm:
|
||||
journal["failure_reason"] = (
|
||||
f"stale concurrency pin: expected {exp_norm[:12]} != live {live_norm[:12]}"
|
||||
)
|
||||
save_phase_journal(journal)
|
||||
return {
|
||||
"success": False,
|
||||
"reason_code": "stale_concurrency_pin",
|
||||
"message": (
|
||||
f"Expected base SHA {exp_norm[:12]} does not match live master SHA {live_norm[:12]} (fail closed)."
|
||||
),
|
||||
"expected_base_sha": expected_base_sha,
|
||||
"live_master_sha": live_master_sha,
|
||||
"exact_next_action": (
|
||||
"Re-evaluate assignment against current live master SHA and retry with updated expected_base_sha."
|
||||
),
|
||||
}
|
||||
|
||||
journal["resolved_base_sha"] = live_master_sha
|
||||
journal["phases"][PHASE_1_REQUEST_ACCEPTED] = {
|
||||
"status": "completed",
|
||||
"live_master_sha": live_master_sha,
|
||||
"expected_base_sha": expected_base_sha,
|
||||
}
|
||||
journal["current_phase"] = PHASE_2_BRANCH_CONFIRMED
|
||||
save_phase_journal(journal)
|
||||
|
||||
if dry_run:
|
||||
return {
|
||||
"success": True,
|
||||
"replayed": True,
|
||||
"message": (
|
||||
f"Idempotent replay: worktree for issue #{issue_number} already bootstrapped at {target_worktree}"
|
||||
),
|
||||
"dry_run": True,
|
||||
"message": f"Dry-run: validated bootstrap intent for issue #{issue_number}",
|
||||
"issue_number": issue_number,
|
||||
"branch_name": target_branch,
|
||||
"worktree_path": target_worktree,
|
||||
"base_sha": existing.get("resolved_base_sha"),
|
||||
"lease_id": existing.get("lease_id"),
|
||||
"assignment_id": existing.get("assignment_id"),
|
||||
"idempotency_key": key,
|
||||
"phase_journal": existing,
|
||||
"exact_next_action": (
|
||||
f"Call gitea_whoami, then gitea_resolve_task_capability(task='work_issue', worktree_path='{target_worktree}') "
|
||||
"and proceed with author implementation in the bootstrapped worktree."
|
||||
),
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"reason_code": "incompatible_idempotency_replay",
|
||||
"message": (
|
||||
f"Idempotency key '{key}' already exists with incompatible parameters "
|
||||
f"(stored: {existing.get('branch_name')}, {existing.get('worktree_path')}; "
|
||||
f"requested: {target_branch}, {target_worktree})"
|
||||
),
|
||||
"exact_next_action": (
|
||||
"Supply a unique idempotency_key or pass compatible parameters."
|
||||
),
|
||||
"base_sha": live_master_sha,
|
||||
"phase_journal": journal,
|
||||
"exact_next_action": "Run without dry_run=True to execute bootstrap.",
|
||||
}
|
||||
|
||||
# Initialize Phase Journal
|
||||
journal = existing or {
|
||||
"idempotency_key": key,
|
||||
"issue_number": issue_number,
|
||||
"assignment_id": assignment_id,
|
||||
"lease_id": lease_id,
|
||||
"expected_base_sha": expected_base_sha,
|
||||
"resolved_base_sha": None,
|
||||
"branch_name": target_branch,
|
||||
"worktree_path": target_worktree,
|
||||
"active_identity": active_identity,
|
||||
"active_profile": active_profile,
|
||||
"owner_session": owner_session,
|
||||
"remote": remote,
|
||||
"org": org,
|
||||
"repo": repo,
|
||||
"phases": {},
|
||||
"artifacts_created": {
|
||||
"branch_created": False,
|
||||
"worktree_dir_created": False,
|
||||
"worktree_registered": False,
|
||||
"lock_created": False,
|
||||
},
|
||||
"current_phase": PHASE_1_REQUEST_ACCEPTED,
|
||||
"completed": False,
|
||||
}
|
||||
|
||||
# Fetch current live master SHA
|
||||
try:
|
||||
rev_res = subprocess.run(
|
||||
["git", "-C", root, "rev-parse", "HEAD"],
|
||||
# Phase 2: BRANCH_CONFIRMED
|
||||
was_branch_created_previously = journal["artifacts_created"].get("branch_created", False)
|
||||
branch_check = subprocess.run(
|
||||
["git", "-C", root, "rev-parse", "--verify", target_branch],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
check=False,
|
||||
)
|
||||
live_master_sha = rev_res.stdout.strip()
|
||||
except Exception as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"reason_code": "git_rev_parse_failed",
|
||||
"message": f"Could not determine repository HEAD: {exc}",
|
||||
"exact_next_action": "Verify repository git state and retry.",
|
||||
}
|
||||
|
||||
# Phase 1: REQUEST_ACCEPTED & Concurrency Pin Check
|
||||
if expected_base_sha:
|
||||
exp_norm = expected_base_sha.strip().lower()
|
||||
live_norm = live_master_sha.lower()
|
||||
if exp_norm != live_norm:
|
||||
journal["failure_reason"] = (
|
||||
f"stale concurrency pin: expected {exp_norm[:12]} != live {live_norm[:12]}"
|
||||
if branch_check.returncode == 0:
|
||||
branch_head = branch_check.stdout.strip()
|
||||
# Verify branch head descends from base
|
||||
anc_check = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
root,
|
||||
"merge-base",
|
||||
"--is-ancestor",
|
||||
live_master_sha,
|
||||
branch_head,
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
save_phase_journal(journal)
|
||||
if anc_check.returncode != 0 and branch_head.lower() != live_master_sha.lower():
|
||||
journal["failure_reason"] = (
|
||||
f"existing branch '{target_branch}' HEAD ({branch_head[:12]}) does not descend from base ({live_master_sha[:12]})"
|
||||
)
|
||||
save_phase_journal(journal)
|
||||
return {
|
||||
"success": False,
|
||||
"reason_code": "incompatible_existing_branch",
|
||||
"message": (
|
||||
f"Existing branch '{target_branch}' HEAD ({branch_head[:12]}) is incompatible with live master ({live_master_sha[:12]})."
|
||||
),
|
||||
"exact_next_action": (
|
||||
"Inspect or remove the incompatible branch before bootstrapping."
|
||||
),
|
||||
}
|
||||
# Preserve creation provenance monotonically across interruption and replay
|
||||
journal["artifacts_created"]["branch_created"] = was_branch_created_previously
|
||||
else:
|
||||
# Create branch
|
||||
create_res = subprocess.run(
|
||||
["git", "-C", root, "branch", target_branch, live_master_sha],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if create_res.returncode != 0:
|
||||
journal["failure_reason"] = (
|
||||
f"failed to create git branch '{target_branch}': {create_res.stderr.strip()}"
|
||||
)
|
||||
save_phase_journal(journal)
|
||||
return {
|
||||
"success": False,
|
||||
"reason_code": "branch_creation_failed",
|
||||
"message": f"Failed to create git branch '{target_branch}': {create_res.stderr.strip()}",
|
||||
"exact_next_action": "Verify branch availability and retry.",
|
||||
}
|
||||
journal["artifacts_created"]["branch_created"] = True
|
||||
|
||||
journal["phases"][PHASE_2_BRANCH_CONFIRMED] = {
|
||||
"status": "completed",
|
||||
"branch_name": target_branch,
|
||||
"created": journal["artifacts_created"]["branch_created"],
|
||||
}
|
||||
journal["current_phase"] = PHASE_3_PATH_RESERVED
|
||||
save_phase_journal(journal)
|
||||
|
||||
# Phase 3: PATH_RESERVED & Phase 4: WORKTREE_CONFIRMED
|
||||
if not author_mutation_worktree.is_path_under_branches(
|
||||
target_worktree, root
|
||||
):
|
||||
journal["failure_reason"] = (
|
||||
f"target_worktree '{target_worktree}' is outside canonical branches/ root"
|
||||
)
|
||||
run_compensating_recovery(journal, root)
|
||||
return {
|
||||
"success": False,
|
||||
"reason_code": "stale_concurrency_pin",
|
||||
"reason_code": "path_outside_canonical_branches_root",
|
||||
"message": (
|
||||
f"Expected base SHA {exp_norm[:12]} does not match live master SHA {live_norm[:12]} (fail closed)."
|
||||
f"Worktree path '{target_worktree}' is outside canonical branches/ root (fail closed)."
|
||||
),
|
||||
"expected_base_sha": expected_base_sha,
|
||||
"live_master_sha": live_master_sha,
|
||||
"exact_next_action": (
|
||||
"Re-evaluate assignment against current live master SHA and retry with updated expected_base_sha."
|
||||
"Provide a worktree_path inside canonical branches/ root, e.g., 'branches/issue-...'"
|
||||
),
|
||||
}
|
||||
|
||||
journal["resolved_base_sha"] = live_master_sha
|
||||
journal["phases"][PHASE_1_REQUEST_ACCEPTED] = {
|
||||
"status": "completed",
|
||||
"live_master_sha": live_master_sha,
|
||||
"expected_base_sha": expected_base_sha,
|
||||
}
|
||||
journal["current_phase"] = PHASE_2_BRANCH_CONFIRMED
|
||||
save_phase_journal(journal)
|
||||
was_dir_created_previously = journal["artifacts_created"].get("worktree_dir_created", False)
|
||||
was_registered_previously = journal["artifacts_created"].get("worktree_registered", False)
|
||||
|
||||
dir_exists = os.path.exists(target_worktree)
|
||||
if dir_exists:
|
||||
# Check porcelain directly
|
||||
porc_res = subprocess.run(
|
||||
["git", "-C", target_worktree, "status", "--porcelain"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
dirty = (
|
||||
parse_dirty_tracked_files(porc_res.stdout)
|
||||
if porc_res.returncode == 0
|
||||
else []
|
||||
)
|
||||
if dirty or (porc_res.returncode == 0 and porc_res.stdout.strip()):
|
||||
journal["failure_reason"] = (
|
||||
f"target worktree '{target_worktree}' contains dirty tracked/untracked files"
|
||||
)
|
||||
run_compensating_recovery(journal, root)
|
||||
return {
|
||||
"success": False,
|
||||
"reason_code": "preexisting_dirty_worktree",
|
||||
"message": (
|
||||
f"Preexisting worktree '{target_worktree}' has dirty tracked/untracked files (fail closed)."
|
||||
),
|
||||
"exact_next_action": (
|
||||
"Clean or stash the pre-existing worktree files before bootstrapping."
|
||||
),
|
||||
}
|
||||
|
||||
# Check registered branch
|
||||
wt_state = issue_lock_worktree.read_worktree_git_state(target_worktree)
|
||||
wt_branch = (wt_state.get("current_branch") or "").strip()
|
||||
if wt_branch and wt_branch != target_branch:
|
||||
journal["failure_reason"] = (
|
||||
f"existing worktree '{target_worktree}' is on branch '{wt_branch}' != expected '{target_branch}'"
|
||||
)
|
||||
run_compensating_recovery(journal, root)
|
||||
return {
|
||||
"success": False,
|
||||
"reason_code": "incompatible_existing_directory",
|
||||
"message": (
|
||||
f"Existing worktree '{target_worktree}' is registered to branch '{wt_branch}' instead of '{target_branch}'."
|
||||
),
|
||||
"exact_next_action": (
|
||||
"Inspect or remove the pre-existing worktree folder before bootstrapping."
|
||||
),
|
||||
}
|
||||
journal["artifacts_created"]["worktree_dir_created"] = was_dir_created_previously
|
||||
journal["artifacts_created"]["worktree_registered"] = (
|
||||
was_registered_previously or was_dir_created_previously
|
||||
)
|
||||
else:
|
||||
wt_add_res = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
root,
|
||||
"worktree",
|
||||
"add",
|
||||
target_worktree,
|
||||
target_branch,
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if wt_add_res.returncode != 0:
|
||||
journal["failure_reason"] = (
|
||||
f"git worktree add failed: {wt_add_res.stderr.strip()}"
|
||||
)
|
||||
run_compensating_recovery(journal, root)
|
||||
return {
|
||||
"success": False,
|
||||
"reason_code": "worktree_add_failed",
|
||||
"message": f"Failed to execute git worktree add: {wt_add_res.stderr.strip()}",
|
||||
"exact_next_action": "Verify git worktree capabilities and retry.",
|
||||
}
|
||||
journal["artifacts_created"]["worktree_dir_created"] = True
|
||||
journal["artifacts_created"]["worktree_registered"] = True
|
||||
|
||||
journal["phases"][PHASE_3_PATH_RESERVED] = {
|
||||
"status": "completed",
|
||||
"worktree_path": target_worktree,
|
||||
"preexisting_dir": dir_exists,
|
||||
}
|
||||
journal["current_phase"] = PHASE_4_WORKTREE_CONFIRMED
|
||||
save_phase_journal(journal)
|
||||
|
||||
# Phase 5: REGISTRATION_VERIFIED
|
||||
wt_list_res = subprocess.run(
|
||||
["git", "-C", root, "worktree", "list", "--porcelain"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
norm_target = os.path.realpath(target_worktree)
|
||||
found_registration = False
|
||||
if wt_list_res.returncode == 0:
|
||||
for block in wt_list_res.stdout.split("\n\n"):
|
||||
lines = block.strip().splitlines()
|
||||
worktree_line = next(
|
||||
(l[9:].strip() for l in lines if l.startswith("worktree ")),
|
||||
None,
|
||||
)
|
||||
if worktree_line and os.path.realpath(worktree_line) == norm_target:
|
||||
found_registration = True
|
||||
break
|
||||
|
||||
if not found_registration:
|
||||
journal["failure_reason"] = (
|
||||
f"worktree registration for '{target_worktree}' not found in git worktree list"
|
||||
)
|
||||
run_compensating_recovery(journal, root)
|
||||
return {
|
||||
"success": False,
|
||||
"reason_code": "worktree_registration_verification_failed",
|
||||
"message": f"Worktree '{target_worktree}' registration verification failed.",
|
||||
"exact_next_action": "Check git worktree list integrity and retry.",
|
||||
}
|
||||
|
||||
journal["phases"][PHASE_4_WORKTREE_CONFIRMED] = {
|
||||
"status": "completed",
|
||||
"worktree_path": target_worktree,
|
||||
}
|
||||
journal["phases"][PHASE_5_REGISTRATION_VERIFIED] = {
|
||||
"status": "completed",
|
||||
"registered": True,
|
||||
}
|
||||
journal["current_phase"] = PHASE_6_STATE_ESTABLISHED
|
||||
save_phase_journal(journal)
|
||||
|
||||
# Phase 6: STATE_ESTABLISHED — Issue Lock Acquisition
|
||||
from datetime import datetime, timezone
|
||||
try:
|
||||
lock_data = {
|
||||
"remote": remote,
|
||||
"org": org or "Scaled-Tech-Consulting",
|
||||
"repo": repo or "Gitea-Tools",
|
||||
"issue_number": issue_number,
|
||||
"branch": target_branch,
|
||||
"branch_name": target_branch,
|
||||
"worktree_path": target_worktree,
|
||||
"owner_session": owner_session or "prgs-author-95048-63667752",
|
||||
"claimant": {
|
||||
"username": active_identity or "jcwalker3",
|
||||
"profile": active_profile or "prgs-author",
|
||||
},
|
||||
"assignment_id": assignment_id,
|
||||
"lease_id": lease_id,
|
||||
"expected_base_sha": live_master_sha,
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
lock_res = issue_lock_store.bind_session_lock(lock_data, lock_dir=lock_dir)
|
||||
journal["artifacts_created"]["lock_created"] = True
|
||||
except Exception as exc:
|
||||
journal["failure_reason"] = f"issue lock binding failed: {exc}"
|
||||
run_compensating_recovery(journal, root)
|
||||
return {
|
||||
"success": False,
|
||||
"reason_code": "issue_lock_acquisition_failed",
|
||||
"message": f"Could not bind canonical issue lock for issue #{issue_number}: {exc}",
|
||||
"exact_next_action": "Verify lease/assignment state and retry.",
|
||||
}
|
||||
|
||||
journal["phases"][PHASE_6_STATE_ESTABLISHED] = {
|
||||
"status": "completed",
|
||||
"lock": lock_res,
|
||||
}
|
||||
journal["phases"][PHASE_7_TRANSITION_COMPLETED] = {
|
||||
"status": "completed",
|
||||
}
|
||||
journal["current_phase"] = PHASE_7_TRANSITION_COMPLETED
|
||||
journal["completed"] = True
|
||||
save_phase_journal(journal)
|
||||
|
||||
if dry_run:
|
||||
return {
|
||||
"success": True,
|
||||
"dry_run": True,
|
||||
"message": f"Dry-run: validated bootstrap intent for issue #{issue_number}",
|
||||
"replayed": False,
|
||||
"message": (
|
||||
f"Successfully bootstrapped author issue worktree for issue #{issue_number} "
|
||||
f"at branch '{target_branch}' and worktree '{target_worktree}'."
|
||||
),
|
||||
"issue_number": issue_number,
|
||||
"branch_name": target_branch,
|
||||
"worktree_path": target_worktree,
|
||||
"base_sha": live_master_sha,
|
||||
"phase_journal": journal,
|
||||
"exact_next_action": "Run without dry_run=True to execute bootstrap.",
|
||||
}
|
||||
|
||||
# Phase 2: BRANCH_CONFIRMED
|
||||
branch_check = subprocess.run(
|
||||
["git", "-C", root, "rev-parse", "--verify", target_branch],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if branch_check.returncode == 0:
|
||||
branch_head = branch_check.stdout.strip()
|
||||
# Verify branch head descends from base
|
||||
anc_check = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
root,
|
||||
"merge-base",
|
||||
"--is-ancestor",
|
||||
live_master_sha,
|
||||
branch_head,
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if anc_check.returncode != 0 and branch_head.lower() != live_master_sha.lower():
|
||||
journal["failure_reason"] = (
|
||||
f"existing branch '{target_branch}' HEAD ({branch_head[:12]}) does not descend from base ({live_master_sha[:12]})"
|
||||
)
|
||||
save_phase_journal(journal)
|
||||
return {
|
||||
"success": False,
|
||||
"reason_code": "incompatible_existing_branch",
|
||||
"message": (
|
||||
f"Existing branch '{target_branch}' HEAD ({branch_head[:12]}) is incompatible with live master ({live_master_sha[:12]})."
|
||||
),
|
||||
"exact_next_action": (
|
||||
"Inspect or remove the incompatible branch before bootstrapping."
|
||||
),
|
||||
}
|
||||
journal["artifacts_created"]["branch_created"] = False
|
||||
else:
|
||||
# Create branch
|
||||
create_res = subprocess.run(
|
||||
["git", "-C", root, "branch", target_branch, live_master_sha],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if create_res.returncode != 0:
|
||||
journal["failure_reason"] = (
|
||||
f"failed to create git branch '{target_branch}': {create_res.stderr.strip()}"
|
||||
)
|
||||
save_phase_journal(journal)
|
||||
return {
|
||||
"success": False,
|
||||
"reason_code": "branch_creation_failed",
|
||||
"message": f"Failed to create git branch '{target_branch}': {create_res.stderr.strip()}",
|
||||
"exact_next_action": "Verify branch availability and retry.",
|
||||
}
|
||||
journal["artifacts_created"]["branch_created"] = True
|
||||
|
||||
journal["phases"][PHASE_2_BRANCH_CONFIRMED] = {
|
||||
"status": "completed",
|
||||
"branch_name": target_branch,
|
||||
"created": journal["artifacts_created"]["branch_created"],
|
||||
}
|
||||
journal["current_phase"] = PHASE_3_PATH_RESERVED
|
||||
save_phase_journal(journal)
|
||||
|
||||
# Phase 3: PATH_RESERVED
|
||||
if not author_mutation_worktree.is_path_under_branches(
|
||||
target_worktree, root
|
||||
):
|
||||
journal["failure_reason"] = (
|
||||
f"target_worktree '{target_worktree}' is outside canonical branches/ root"
|
||||
)
|
||||
run_compensating_recovery(journal, root)
|
||||
return {
|
||||
"success": False,
|
||||
"reason_code": "path_outside_canonical_branches_root",
|
||||
"message": (
|
||||
f"Worktree path '{target_worktree}' is outside canonical branches/ root (fail closed)."
|
||||
),
|
||||
"exact_next_action": (
|
||||
"Provide a worktree_path inside canonical branches/ root, e.g., 'branches/issue-...'"
|
||||
),
|
||||
}
|
||||
|
||||
dir_exists = os.path.exists(target_worktree)
|
||||
if dir_exists:
|
||||
# Check porcelain directly
|
||||
porc_res = subprocess.run(
|
||||
["git", "-C", target_worktree, "status", "--porcelain"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
dirty = (
|
||||
parse_dirty_tracked_files(porc_res.stdout)
|
||||
if porc_res.returncode == 0
|
||||
else []
|
||||
)
|
||||
if dirty or (porc_res.returncode == 0 and porc_res.stdout.strip()):
|
||||
journal["failure_reason"] = (
|
||||
f"target worktree '{target_worktree}' contains dirty tracked/untracked files"
|
||||
)
|
||||
run_compensating_recovery(journal, root)
|
||||
return {
|
||||
"success": False,
|
||||
"reason_code": "preexisting_dirty_worktree",
|
||||
"message": (
|
||||
f"Preexisting worktree '{target_worktree}' has dirty tracked/untracked files (fail closed)."
|
||||
),
|
||||
"exact_next_action": (
|
||||
"Clean or stash the pre-existing worktree files before bootstrapping."
|
||||
),
|
||||
}
|
||||
|
||||
# Check registered branch
|
||||
wt_state = issue_lock_worktree.read_worktree_git_state(target_worktree)
|
||||
wt_branch = (wt_state.get("current_branch") or "").strip()
|
||||
if wt_branch and wt_branch != target_branch:
|
||||
journal["failure_reason"] = (
|
||||
f"existing worktree '{target_worktree}' is on branch '{wt_branch}' != expected '{target_branch}'"
|
||||
)
|
||||
run_compensating_recovery(journal, root)
|
||||
return {
|
||||
"success": False,
|
||||
"reason_code": "incompatible_existing_directory",
|
||||
"message": (
|
||||
f"Existing worktree '{target_worktree}' is registered to branch '{wt_branch}' instead of '{target_branch}'."
|
||||
),
|
||||
"exact_next_action": (
|
||||
"Inspect or remove the pre-existing worktree folder before bootstrapping."
|
||||
),
|
||||
}
|
||||
journal["artifacts_created"]["worktree_dir_created"] = False
|
||||
else:
|
||||
journal["artifacts_created"]["worktree_dir_created"] = True
|
||||
|
||||
journal["phases"][PHASE_3_PATH_RESERVED] = {
|
||||
"status": "completed",
|
||||
"worktree_path": target_worktree,
|
||||
"preexisting_dir": dir_exists,
|
||||
}
|
||||
journal["current_phase"] = PHASE_4_WORKTREE_CONFIRMED
|
||||
save_phase_journal(journal)
|
||||
|
||||
# Phase 4: WORKTREE_CONFIRMED & Phase 5: REGISTRATION_VERIFIED
|
||||
if journal["artifacts_created"]["worktree_dir_created"]:
|
||||
wt_add_res = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
root,
|
||||
"worktree",
|
||||
"add",
|
||||
target_worktree,
|
||||
target_branch,
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if wt_add_res.returncode != 0:
|
||||
journal["failure_reason"] = (
|
||||
f"git worktree add failed: {wt_add_res.stderr.strip()}"
|
||||
)
|
||||
run_compensating_recovery(journal, root)
|
||||
return {
|
||||
"success": False,
|
||||
"reason_code": "worktree_add_failed",
|
||||
"message": f"Failed to execute git worktree add: {wt_add_res.stderr.strip()}",
|
||||
"exact_next_action": "Verify git worktree capabilities and retry.",
|
||||
}
|
||||
journal["artifacts_created"]["worktree_registered"] = True
|
||||
|
||||
# Verify registration in git worktree list
|
||||
wt_list_res = subprocess.run(
|
||||
["git", "-C", root, "worktree", "list", "--porcelain"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
norm_target = os.path.realpath(target_worktree)
|
||||
found_registration = False
|
||||
if wt_list_res.returncode == 0:
|
||||
for block in wt_list_res.stdout.split("\n\n"):
|
||||
lines = block.strip().splitlines()
|
||||
worktree_line = next(
|
||||
(l[9:].strip() for l in lines if l.startswith("worktree ")),
|
||||
None,
|
||||
)
|
||||
if worktree_line and os.path.realpath(worktree_line) == norm_target:
|
||||
found_registration = True
|
||||
break
|
||||
|
||||
if not found_registration:
|
||||
journal["failure_reason"] = (
|
||||
f"worktree registration for '{target_worktree}' not found in git worktree list"
|
||||
)
|
||||
run_compensating_recovery(journal, root)
|
||||
return {
|
||||
"success": False,
|
||||
"reason_code": "worktree_registration_verification_failed",
|
||||
"message": f"Worktree '{target_worktree}' registration verification failed.",
|
||||
"exact_next_action": "Check git worktree list integrity and retry.",
|
||||
}
|
||||
|
||||
journal["phases"][PHASE_4_WORKTREE_CONFIRMED] = {
|
||||
"status": "completed",
|
||||
"worktree_path": target_worktree,
|
||||
}
|
||||
journal["phases"][PHASE_5_REGISTRATION_VERIFIED] = {
|
||||
"status": "completed",
|
||||
"registered": True,
|
||||
}
|
||||
journal["current_phase"] = PHASE_6_STATE_ESTABLISHED
|
||||
save_phase_journal(journal)
|
||||
|
||||
# Phase 6: STATE_ESTABLISHED — Issue Lock Acquisition
|
||||
from datetime import datetime, timezone
|
||||
try:
|
||||
lock_data = {
|
||||
"remote": remote,
|
||||
"org": org or "Scaled-Tech-Consulting",
|
||||
"repo": repo or "Gitea-Tools",
|
||||
"issue_number": issue_number,
|
||||
"branch": target_branch,
|
||||
"branch_name": target_branch,
|
||||
"worktree_path": target_worktree,
|
||||
"owner_session": owner_session or "prgs-author-95048-63667752",
|
||||
"claimant": {
|
||||
"username": active_identity or "jcwalker3",
|
||||
"profile": active_profile or "prgs-author",
|
||||
},
|
||||
"assignment_id": assignment_id,
|
||||
"lease_id": lease_id,
|
||||
"expected_base_sha": live_master_sha,
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
"assignment_id": assignment_id,
|
||||
"idempotency_key": key,
|
||||
"lock_state": lock_res,
|
||||
"phase_journal": journal,
|
||||
"exact_next_action": (
|
||||
f"Call gitea_whoami, then gitea_resolve_task_capability(task='work_issue', worktree_path='{target_worktree}') "
|
||||
"and proceed with author implementation in the bootstrapped worktree."
|
||||
),
|
||||
}
|
||||
lock_res = issue_lock_store.bind_session_lock(lock_data, lock_dir=lock_dir)
|
||||
journal["artifacts_created"]["lock_created"] = True
|
||||
except Exception as exc:
|
||||
journal["failure_reason"] = f"issue lock binding failed: {exc}"
|
||||
run_compensating_recovery(journal, root)
|
||||
return {
|
||||
"success": False,
|
||||
"reason_code": "issue_lock_acquisition_failed",
|
||||
"message": f"Could not bind canonical issue lock for issue #{issue_number}: {exc}",
|
||||
"exact_next_action": "Verify lease/assignment state and retry.",
|
||||
}
|
||||
|
||||
journal["phases"][PHASE_6_STATE_ESTABLISHED] = {
|
||||
"status": "completed",
|
||||
"lock": lock_res,
|
||||
}
|
||||
journal["phases"][PHASE_7_TRANSITION_COMPLETED] = {
|
||||
"status": "completed",
|
||||
}
|
||||
journal["current_phase"] = PHASE_7_TRANSITION_COMPLETED
|
||||
journal["completed"] = True
|
||||
save_phase_journal(journal)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"replayed": False,
|
||||
"message": (
|
||||
f"Successfully bootstrapped author issue worktree for issue #{issue_number} "
|
||||
f"at branch '{target_branch}' and worktree '{target_worktree}'."
|
||||
),
|
||||
"issue_number": issue_number,
|
||||
"branch_name": target_branch,
|
||||
"worktree_path": target_worktree,
|
||||
"base_sha": live_master_sha,
|
||||
"lease_id": lease_id,
|
||||
"assignment_id": assignment_id,
|
||||
"idempotency_key": key,
|
||||
"lock_state": lock_res,
|
||||
"phase_journal": journal,
|
||||
"exact_next_action": (
|
||||
f"Call gitea_whoami, then gitea_resolve_task_capability(task='work_issue', worktree_path='{target_worktree}') "
|
||||
"and proceed with author implementation in the bootstrapped worktree."
|
||||
),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user