Files
Gitea-Tools/author_issue_bootstrap.py
sysadminandClaude Opus 4.8 1aa351718a fix(author): derive AC7 guidance from the state compensation actually leaves (#953)
Review 632 F2. The bootstrap AC7 read-back refusal calls
`run_compensating_recovery` and *then* reported
`author_lock_contract.recommended_action(contract)` — advice computed from the
malformed lock that provoked the rollback, not from the state the rollback
left. Compensation releases the lock, removes the worktree (always clean
there, no implementation bytes having been written), and deletes the branch,
so an author following that advice got `no_durable_lock` from
`gitea_recover_incomplete_bootstrap_lock` and, had the lock survived,
`worktree_invalid` instead; the `gitea_lock_issue` half of the same sentence
cannot bind a worktree that no longer exists. Two refusals in a row for a
state a plain bootstrap retry fixes — the unexecutable-guidance failure class
this issue exists to remove, reintroduced on the new fail-closed path.

Investigating that path surfaced why the "clean retry" state was in practice
unreachable: `run_compensating_recovery` has called
`issue_lock_store.release_session_lock` since #850, and that function has
never existed. The `AttributeError` landed in a bare `except Exception: pass`,
so every rollback removed the branch and worktree and silently left the lock
behind — precisely the uninspectable, unrecoverable state #953 is about
(`gitea_recover_incomplete_bootstrap_lock` refuses `worktree_invalid`,
`gitea_lock_issue` has no worktree to bind). Confirmed dead at the pinned base
`82d71b77`, not introduced by this branch.

`release_session_lock` is therefore implemented: it removes exactly one
durable lock whose recorded `owner_session` matches the caller's, keyed by
repository when known, refusing on zero or multiple matches so no caller can
delete a lock it does not own and an ambiguous directory is never guessed at.
Bootstrap phase journals and session pointers that share the directory are
excluded by shape. The flock sidecar is deliberately left alone. The caller no
longer swallows a release failure; it records `lock_release_failed:...`.

`assess_post_compensation_state` then classifies from directly observed
durable state — lock file, worktree directory, and branch ref — rather than
from the journal's `rolled_back` list, which records only what compensation
attempted. Three distinct states: `complete` (nothing remains),
`partial` (rollback ran, artifacts survive by design or because a step
errored), `failed` (rollback never completed, so nothing is proven removed).

`post_compensation_action` answers for exactly what survives:

  complete                      -> re-run gitea_bootstrap_author_issue_worktree
  lock + branch + worktree      -> gitea_recover_incomplete_bootstrap_lock
  branch + worktree, no lock    -> gitea_lock_issue (still base-equivalent)
  lock only, worktree gone      -> gitea_inspect_issue_lock_contract
  branch only                   -> gitea_inspect_issue_lock_contract, then retry
  rollback did not complete     -> gitea_inspect_issue_lock_contract

No branch names an artifact the classification says is gone, and a failed
rollback step is stated rather than presented as an intentional outcome. The
refusal payload carries `compensating_recovery` and `post_compensation_state`
alongside the derived `exact_next_action`, still with
`implementation_allowed: false`.

Also removes the dead `SOURCE_BOOTSTRAP_LOCK_RECOVERY` constant (review 632
F3), which had no readers and implied a second lock source; the deliberate
reuse of `SOURCE_LOCK_ISSUE` is now stated as a comment. The #447 guard and
`SANCTIONED_LOCK_SOURCES` remain untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-28 02:08:21 -04:00

1380 lines
56 KiB
Python

"""Sanctioned author issue worktree bootstrap for allocated issues (#850).
Bootstraps an allocated author branch, canonical worktree under ``branches/``,
worktree registration, issue lock, and lease/assignment binding without
caller-side Git, Bash, or helper scripts.
Features:
1. Durable phase journal with read-after-write evidence for every phase.
2. Idempotent replay handling via idempotency keys.
3. Authoritative expected-base / concurrency-pin validation.
4. Typed stale-pin refusal without silent rebasing or repointing.
5. Canonical branches-root enforcement (path MUST be inside branches/).
6. Preexisting work preservation (dirty tracked/untracked check, foreign ownership refusal).
7. Compensating recovery limited strictly to artifacts created by this transition.
8. Satisfiable exact_next_action for MCP scheduled workers.
"""
from __future__ import annotations
import json
import os
import shutil
import subprocess
from typing import Any, Mapping
import author_lock_contract
import author_mutation_worktree
import control_plane_db
import issue_lock_store
import issue_lock_worktree
import lease_lifecycle
from reviewer_worktree import parse_dirty_tracked_files
import task_capability_map
BOOTSTRAP_TASKS = frozenset(
{
"bootstrap_author_issue_worktree",
"gitea_bootstrap_author_issue_worktree",
}
)
PHASE_1_REQUEST_ACCEPTED = "1_request_accepted"
PHASE_2_BRANCH_CONFIRMED = "2_branch_confirmed"
PHASE_3_PATH_RESERVED = "3_path_reserved"
PHASE_4_WORKTREE_CONFIRMED = "4_worktree_confirmed"
PHASE_5_REGISTRATION_VERIFIED = "5_registration_verified"
PHASE_6_STATE_ESTABLISHED = "6_state_established"
PHASE_7_TRANSITION_COMPLETED = "7_transition_completed"
PHASE_COMPENSATING_RECOVERY = "compensating_recovery"
JOURNAL_DIR_NAME = "bootstrap-journals"
def is_author_issue_bootstrap_task(task: str | None) -> bool:
"""True when *task* is the author issue worktree bootstrap task."""
return (task or "").strip() in BOOTSTRAP_TASKS
def get_journal_dir(override: str | None = None) -> str:
"""Return the root directory for durable bootstrap phase journals."""
if override:
path = override
elif os.environ.get("GITEA_BOOTSTRAP_JOURNAL_DIR"):
path = os.environ["GITEA_BOOTSTRAP_JOURNAL_DIR"]
else:
cache_dir = os.path.expanduser("~/.cache/gitea-tools")
path = os.path.join(cache_dir, JOURNAL_DIR_NAME)
os.makedirs(path, exist_ok=True)
return path
def _journal_file_path(idempotency_key: str, journal_dir: str | None = None) -> str:
safe_key = "".join(
c if c.isalnum() or c in ("-", "_", ".") else "_"
for c in idempotency_key
)
return os.path.join(get_journal_dir(journal_dir), f"{safe_key}.json")
def load_phase_journal(
idempotency_key: str, journal_dir: str | None = None
) -> dict[str, Any] | None:
"""Load a durable phase journal if it exists."""
path = _journal_file_path(idempotency_key, journal_dir=journal_dir)
if not os.path.isfile(path):
return None
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return None
def save_phase_journal(
journal: dict[str, Any], journal_dir: str | None = None
) -> None:
"""Persist a durable phase journal with write-through file sync."""
key = journal["idempotency_key"]
path = _journal_file_path(key, journal_dir=journal_dir)
tmp_path = f"{path}.tmp.{os.getpid()}"
with open(tmp_path, "w", encoding="utf-8") as f:
json.dump(journal, f, indent=2, sort_keys=True)
os.replace(tmp_path, path)
def derive_default_idempotency_key(
remote: str,
org: str | None,
repo: str | None,
issue_number: int,
assignment_id: str | None = None,
lease_id: str | None = None,
) -> str:
parts = [
"bootstrap",
(remote or "prgs").strip(),
(org or "Scaled-Tech-Consulting").strip(),
(repo or "Gitea-Tools").strip(),
f"issue-{issue_number}",
]
if assignment_id:
parts.append(assignment_id.strip())
if lease_id:
parts.append(lease_id.strip())
return ":".join(parts)
def _verify_assignment_and_lease_ids(
*,
assignment_id: str | None,
lease_id: str | None,
issue_number: int,
owner_session: str,
remote: str,
org: str | None,
repo: str | None,
db: control_plane_db.ControlPlaneDB | None = None,
) -> dict[str, Any] | None:
"""Fail closed when caller-supplied assignment/lease IDs are unverified (#531 F4).
Both IDs are optional together. When either is supplied, both must be
present and must resolve to the same live control-plane lease/assignment
bound to this issue and owner session. Fabricated identifiers never pass.
"""
asn = (assignment_id or "").strip()
lid = (lease_id or "").strip()
if not asn and not lid:
return None
if not asn or not lid:
return {
"success": False,
"reason_code": "incomplete_assignment_lease_ids",
"message": (
"assignment_id and lease_id must be supplied together when "
"either is provided (fail closed)."
),
"exact_next_action": (
"Pass both identifiers from allocate_next_work / control-plane "
"assignment proof, or omit both."
),
}
try:
store = db if db is not None else control_plane_db.ControlPlaneDB()
state = store.get_lease_workflow_state(lid)
except Exception as exc:
return {
"success": False,
"reason_code": "assignment_lease_lookup_failed",
"message": f"Could not verify assignment/lease against control plane: {exc}",
"exact_next_action": (
"Ensure the control-plane DB is available and retry with live IDs."
),
}
if not state or not state.get("lease"):
return {
"success": False,
"reason_code": "unknown_lease_id",
"message": f"lease_id '{lid}' is not present in the control plane (fail closed).",
"exact_next_action": "Pass a live lease_id from control-plane assignment.",
}
lease = state["lease"]
assignment = state.get("assignment") or {}
work = state.get("work_item") or {}
recorded_asn = str(assignment.get("assignment_id") or "").strip()
if recorded_asn and recorded_asn != asn:
return {
"success": False,
"reason_code": "assignment_lease_mismatch",
"message": (
f"assignment_id '{asn}' does not match lease '{lid}' "
f"(recorded assignment '{recorded_asn}') (fail closed)."
),
"exact_next_action": "Re-read allocate_next_work proof and pass matching IDs.",
}
if not recorded_asn:
# Some lease rows may not yet have an assignment join; still require
# the lease itself to exist and bind to the claimed session/issue.
pass
# #943 review 622 B2: a lease that is no longer live confers no ownership.
# Existence alone previously satisfied this gate, so a released or expired
# lease could still authorize a bootstrap for a claim its session had given
# up. Checked before the session comparison so the reason names the real
# problem rather than reporting a mismatch.
from datetime import datetime, timezone
lease_status = str(lease.get("status") or "").strip().lower()
if lease_status and lease_status != "active":
return {
"success": False,
"reason_code": "lease_not_live",
"message": (
f"lease_id '{lid}' is '{lease_status}', not active; a lease that "
"is not live confers no ownership (fail closed)."
),
"exact_next_action": (
"Re-allocate the work item and pass the live assignment/lease pair."
),
}
expires_raw = str(lease.get("expires_at") or "").strip()
if expires_raw:
try:
expires_at = datetime.fromisoformat(expires_raw.replace("Z", "+00:00"))
except ValueError:
return {
"success": False,
"reason_code": "lease_not_live",
"message": (
f"lease_id '{lid}' records an unparseable expiry "
f"'{expires_raw}' (fail closed)."
),
"exact_next_action": (
"Re-allocate the work item and pass the live "
"assignment/lease pair."
),
}
if expires_at.tzinfo is None:
expires_at = expires_at.replace(tzinfo=timezone.utc)
if expires_at <= datetime.now(timezone.utc):
return {
"success": False,
"reason_code": "lease_not_live",
"message": (
f"lease_id '{lid}' expired at {expires_raw}; an expired lease "
"confers no ownership (fail closed)."
),
"exact_next_action": (
"Reclaim or re-allocate the lease, then retry with the live pair."
),
}
lease_session = str(lease.get("session_id") or "").strip()
if lease_session and lease_session != owner_session:
return {
"success": False,
"reason_code": "lease_session_mismatch",
"message": (
f"lease_id '{lid}' is owned by session '{lease_session}', not "
f"'{owner_session}' (fail closed)."
),
"exact_next_action": "Use the session that holds the lease, or re-allocate.",
}
# Bind issue number when the work item records one.
work_number = work.get("number") or work.get("issue_number") or assignment.get("issue_number")
try:
if work_number is not None and int(work_number) != int(issue_number):
return {
"success": False,
"reason_code": "lease_issue_mismatch",
"message": (
f"lease_id '{lid}' is bound to issue #{work_number}, not "
f"#{issue_number} (fail closed)."
),
"exact_next_action": "Pass the lease issued for this issue number.",
}
except (TypeError, ValueError):
pass
_ = (remote, org, repo) # reserved for host-scoped DBs
return None
def _branch_exists(canonical_repo_root: str, branch_name: str) -> bool:
"""Whether *branch_name* still resolves in the canonical checkout (#953 F2).
Used after compensating recovery to observe what survived rather than infer
it from the journal. Fails closed to ``True``: an unobservable branch is
reported as present, so the recommendation stays conservative rather than
telling an author to re-bootstrap over something that may still be there.
"""
if not branch_name:
return False
try:
res = subprocess.run(
[
"git",
"-C",
canonical_repo_root,
"rev-parse",
"--verify",
"--quiet",
f"refs/heads/{branch_name}",
],
capture_output=True,
text=True,
check=False,
)
except Exception:
return True
return res.returncode == 0
def run_compensating_recovery(
journal: dict[str, Any],
canonical_repo_root: str,
journal_dir: str | None = None,
*,
db: control_plane_db.ControlPlaneDB | None = None,
) -> dict[str, Any]:
"""Execute compensating recovery for artifacts created by this transition only."""
artifacts = journal.get("artifacts_created") or {}
pending = journal.get("pending_creations") or {}
rolled_back: list[str] = []
worktree_path = journal.get("worktree_path")
branch_name = journal.get("branch_name")
# Roll back workflow lease/assignment when this transition bound one (#531 F5).
lease_id = str(journal.get("lease_id") or "").strip()
session_id = str(journal.get("owner_session") or "").strip()
if lease_id and session_id:
try:
store = db if db is not None else control_plane_db.ControlPlaneDB()
lease_lifecycle.release_lease(
store, lease_id=lease_id, session_id=session_id
)
rolled_back.append(f"lease:{lease_id}")
except Exception as exc:
rolled_back.append(f"lease_release_failed:{lease_id}:{type(exc).__name__}")
# Roll back issue lock if created
if artifacts.get("lock_created") or pending.get("lock"):
issue_num = journal.get("issue_number")
if issue_num and session_id:
try:
issue_lock_store.release_session_lock(
issue_number=issue_num,
session=session_id,
lock_dir=journal_dir,
remote=journal.get("remote"),
# The same defaults the lock was written under, so the
# rollback targets the exact file bind_session_lock keyed.
org=journal.get("org") or "Scaled-Tech-Consulting",
repo=journal.get("repo") or "Gitea-Tools",
)
rolled_back.append(f"lock:issue-{issue_num}")
except Exception as exc:
# #953 F2: a swallowed failure here is what made the rollback
# report success while leaving an unrecoverable lock behind.
# Record it so the post-compensation classification can see the
# lock survived and recommend accordingly.
rolled_back.append(
f"lock_release_failed:issue-{issue_num}:{type(exc).__name__}"
)
artifacts["lock_created"] = False
worktree_created = (
artifacts.get("worktree_registered")
or artifacts.get("worktree_dir_created")
or (pending.get("worktree_path") == worktree_path and worktree_path)
)
if worktree_created and worktree_path:
if os.path.exists(worktree_path):
# Re-verify cleanliness before destructive removal (F-4)
porc_res = subprocess.run(
["git", "-C", worktree_path, "status", "--porcelain"],
capture_output=True,
text=True,
check=False,
)
is_dirty = porc_res.returncode == 0 and bool(porc_res.stdout.strip())
if is_dirty:
rolled_back.append(f"worktree_path_preserved_dirty:{worktree_path}")
else:
try:
subprocess.run(
[
"git",
"-C",
canonical_repo_root,
"worktree",
"remove",
"--force",
worktree_path,
],
capture_output=True,
text=True,
check=False,
)
except Exception:
pass
if os.path.exists(worktree_path):
shutil.rmtree(worktree_path, ignore_errors=True)
try:
subprocess.run(
["git", "-C", canonical_repo_root, "worktree", "prune"],
capture_output=True,
text=True,
check=False,
)
except Exception:
pass
rolled_back.append(f"worktree_path:{worktree_path}")
else:
rolled_back.append(f"worktree_path:{worktree_path}")
branch_created = (
artifacts.get("branch_created")
or (pending.get("branch_name") == branch_name and branch_name)
)
if branch_created and branch_name:
try:
res = subprocess.run(
[
"git",
"-C",
canonical_repo_root,
"rev-parse",
"--verify",
branch_name,
],
capture_output=True,
text=True,
check=False,
)
if res.returncode == 0:
# Check for author commits on branch before branch deletion (F-4)
resolved_base = journal.get("resolved_base_sha") or "master"
rev_list_res = subprocess.run(
[
"git",
"-C",
canonical_repo_root,
"rev-list",
f"{resolved_base}..{branch_name}",
],
capture_output=True,
text=True,
check=False,
)
has_commits = rev_list_res.returncode == 0 and bool(rev_list_res.stdout.strip())
if has_commits:
rolled_back.append(f"branch_preserved_commits:{branch_name}")
else:
subprocess.run(
[
"git",
"-C",
canonical_repo_root,
"branch",
"-D",
branch_name,
],
capture_output=True,
text=True,
check=False,
)
rolled_back.append(f"branch:{branch_name}")
except Exception:
pass
recovery_info = {
"executed": True,
"rolled_back": rolled_back,
"reason": journal.get("failure_reason"),
}
journal["compensating_recovery"] = recovery_info
journal["current_phase"] = PHASE_COMPENSATING_RECOVERY
save_phase_journal(journal, journal_dir=journal_dir)
return recovery_info
def _normalize_sha(value: str | None) -> str | None:
"""Normalize a Git object id for comparison, or ``None`` when unknown."""
normalized = (value or "").strip().lower()
return normalized or None
def _author_bootstrap_assessment(
*,
not_applicable: bool,
allowed: bool,
block: bool,
reasons: list[str],
workspace: str,
root: str,
branch: str | None,
dirty: list[str],
under_branches: bool,
bootstrap_path: str | None = None,
local_head_sha: str | None = None,
remote_master_sha: str | None = None,
exact_next_action: str | None = None,
) -> dict[str, Any]:
"""Structured author-bootstrap assessment consumable by bootstrap_permits (#892).
Field shape mirrors :func:`create_issue_bootstrap._result` so the shared
``bootstrap_permits_control_checkout`` predicate can prove control-checkout
eligibility for ``gitea_bootstrap_author_issue_worktree`` the same way it
does for ``create_issue``. Allowed control assessments must use empty
``reasons`` — narrative belongs in other fields, not the refusal list.
"""
local_tip = _normalize_sha(local_head_sha)
remote_tip = _normalize_sha(remote_master_sha)
base_tips_verified = bool(local_tip and remote_tip and local_tip == remote_tip)
return {
"not_applicable": not_applicable,
"allowed": allowed,
"block": block,
"proven": bool(allowed and not block and not not_applicable),
"reasons": list(reasons),
"workspace_path": workspace,
"canonical_repo_root": root,
"current_branch": branch,
"dirty_files": list(dirty),
"under_branches": under_branches,
"exact_next_action": exact_next_action,
"bootstrap_path": bootstrap_path,
"task_scope": "author_issue_bootstrap",
"local_head_sha": local_tip,
"remote_master_sha": remote_tip,
"base_tips_verified": base_tips_verified,
}
EXACT_NEXT_ACTION_AUTHOR_BOOTSTRAP = (
"Restore the canonical control checkout to a clean accepted base branch "
"(master/main/dev) that matches live master, with no tracked local edits. "
"Re-resolve bootstrap_author_issue_worktree, then re-run "
"gitea_bootstrap_author_issue_worktree from that clean control checkout. "
"Do not use shell git worktree add as the primary path once bootstrap is healthy."
)
def assess_author_issue_bootstrap(
*,
workspace_path: str,
canonical_repo_root: str,
current_branch: str | None = None,
head_sha: str | None = None,
porcelain_status: str = "",
remote_master_sha: str | None = None,
remote_master_sha_error: str | None = None,
task: str | None = None,
) -> dict[str, Any]:
"""Assess whether author issue worktree bootstrap may proceed from control or worktree root.
#892: control-checkout successes emit the full field set required by
``create_issue_bootstrap.bootstrap_permits_control_checkout`` (empty reasons,
task_scope, base tip proof, binding paths) so the #274/#604 guards can
waive control-checkout for this one sanctioned bootstrap task.
"""
root = os.path.realpath(canonical_repo_root or "")
workspace = os.path.realpath(workspace_path or root or ".")
branch = (current_branch or "").strip()
dirty = parse_dirty_tracked_files(porcelain_status or "")
under_branches = (
author_mutation_worktree.is_path_under_branches(workspace, root)
if root
else False
)
local_tip = _normalize_sha(head_sha)
remote_tip = _normalize_sha(remote_master_sha)
if not is_author_issue_bootstrap_task(task):
return _author_bootstrap_assessment(
not_applicable=True,
allowed=False,
block=False,
reasons=["task is not author_issue_bootstrap"],
workspace=workspace,
root=root,
branch=branch or None,
dirty=dirty,
under_branches=under_branches,
)
# Already under branches/: ordinary #274 path applies; not a control waiver.
if under_branches:
return _author_bootstrap_assessment(
not_applicable=True,
allowed=False,
block=False,
reasons=["workspace is under branches/; ordinary #274 path applies"],
workspace=workspace,
root=root,
branch=branch or None,
dirty=dirty,
under_branches=True,
bootstrap_path="existing_branches_worktree",
local_head_sha=local_tip,
remote_master_sha=remote_tip,
)
reasons: list[str] = []
if not root or workspace != root:
reasons.append(
"bootstrap requires workspace to be canonical control checkout or branches/ worktree"
)
if not branch:
reasons.append(
"control checkout is detached HEAD; expected an accepted base branch "
f"({', '.join(sorted(author_mutation_worktree.BASE_BRANCHES))})"
)
elif branch not in author_mutation_worktree.BASE_BRANCHES:
reasons.append(
f"control checkout branch '{branch}' is not an accepted base branch "
f"({', '.join(sorted(author_mutation_worktree.BASE_BRANCHES))})"
)
if dirty:
reasons.append(
f"control checkout has tracked local edits: {', '.join(dirty[:5])}"
)
# Fail closed on missing tip proof (same bar as create_issue bootstrap #757).
if not local_tip:
reasons.append(
"control checkout HEAD SHA is unknown; base equivalence to live "
"master cannot be proven (fail closed)"
)
resolver_error = (remote_master_sha_error or "").strip() or None
if resolver_error:
reasons.append(
f"live master tip could not be resolved ({resolver_error}); "
"base equivalence cannot be proven (fail closed)"
)
elif not remote_tip:
reasons.append(
"live master tip is unknown; base equivalence cannot be proven "
"(fail closed)"
)
elif local_tip and remote_tip and local_tip != remote_tip:
reasons.append(
f"control checkout HEAD ({local_tip[:12]}) != live master tip "
f"({remote_tip[:12]})"
)
if reasons:
return _author_bootstrap_assessment(
not_applicable=False,
allowed=False,
block=True,
reasons=reasons,
workspace=workspace,
root=root,
branch=branch or None,
dirty=dirty,
under_branches=False,
local_head_sha=local_tip,
remote_master_sha=remote_tip,
exact_next_action=EXACT_NEXT_ACTION_AUTHOR_BOOTSTRAP,
)
# Allowed: empty reasons so bootstrap_permits_control_checkout can pass.
return _author_bootstrap_assessment(
not_applicable=False,
allowed=True,
block=False,
reasons=[],
workspace=workspace,
root=root,
branch=branch or None,
dirty=dirty,
under_branches=False,
bootstrap_path="clean_canonical_control_checkout",
local_head_sha=local_tip,
remote_master_sha=remote_tip,
exact_next_action=(
"Call gitea_bootstrap_author_issue_worktree with the allocated "
"issue/lease pins; it will create the branches/ worktree and lock."
),
)
import fcntl
import stat
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 = os.path.realpath(get_journal_dir(journal_dir))
if not os.path.isdir(lock_dir):
raise RuntimeError(f"Lock directory '{lock_dir}' does not exist or is not a directory")
raw_lock_path = os.path.abspath(os.path.join(lock_dir, f"{safe_key}.lock"))
try:
common = os.path.commonpath([lock_dir, os.path.dirname(raw_lock_path)])
except Exception:
common = None
if common != lock_dir:
raise RuntimeError(f"Lock path '{raw_lock_path}' escapes canonical lock directory '{lock_dir}'")
self.lock_path = raw_lock_path
self.fd = None
def __enter__(self):
if os.path.islink(self.lock_path):
raise RuntimeError(f"Refusing lock acquisition: lock path '{self.lock_path}' is a symlink")
flags = os.O_RDWR | os.O_CREAT
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
if hasattr(os, "O_CLOEXEC"):
flags |= os.O_CLOEXEC
try:
fd = os.open(self.lock_path, flags, 0o600)
except OSError as exc:
raise RuntimeError(f"Failed to open lock file safely '{self.lock_path}': {exc}") from exc
st = os.fstat(fd)
if not stat.S_ISREG(st.st_mode):
os.close(fd)
raise RuntimeError(f"Lock target '{self.lock_path}' is not a regular file")
self.fd = fd
fcntl.flock(self.fd, fcntl.LOCK_EX)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if self.fd is not None:
try:
fcntl.flock(self.fd, fcntl.LOCK_UN)
except Exception:
pass
try:
os.close(self.fd)
except Exception:
pass
self.fd = None
def bootstrap_author_issue_worktree(
*,
issue_number: int,
canonical_repo_root: str,
assignment_id: str | None = None,
lease_id: str | None = None,
expected_base_sha: str | None = None,
branch_name: str | None = None,
worktree_path: str | None = None,
idempotency_key: str | None = None,
remote: str = "prgs",
host: str | None = None,
org: str | None = "Scaled-Tech-Consulting",
repo: str | None = "Gitea-Tools",
active_identity: str | None = "jcwalker3",
active_profile: str | None = "prgs-author",
owner_session: str | None = None,
lock_dir: str | None = None,
dry_run: bool = False,
) -> dict[str, Any]:
"""Execute the sanctioned author issue worktree bootstrap transition."""
root = os.path.realpath(canonical_repo_root)
session = (owner_session or "").strip()
if not session:
return {
"success": False,
"reason_code": "missing_owner_session",
"message": "Missing required owner_session parameter (fail closed). Session identifier cannot be fabricated or defaulted.",
"exact_next_action": (
"Pass explicit owner_session resolved from gitea_whoami or session context."
),
}
if active_identity is None or not str(active_identity).strip():
return {
"success": False,
"reason_code": "missing_active_identity",
"message": "Missing required active_identity parameter (fail closed). Identity cannot be fabricated or defaulted.",
"exact_next_action": "Pass explicit active_identity resolved from gitea_whoami.",
}
identity = str(active_identity).strip()
if active_profile is None or not str(active_profile).strip():
return {
"success": False,
"reason_code": "missing_active_profile",
"message": "Missing required active_profile parameter (fail closed). Profile cannot be fabricated or defaulted.",
"exact_next_action": "Pass explicit active_profile resolved from gitea_whoami.",
}
profile = str(active_profile).strip()
# Derive standard inputs
expected_pattern = f"issue-{issue_number}"
target_branch = (branch_name or "").strip()
if not target_branch:
target_branch = f"fix/issue-{issue_number}-native-mcp-bootstrap"
elif expected_pattern not in target_branch:
return {
"success": False,
"reason_code": "invalid_branch_name",
"message": (
f"Branch name '{target_branch}' must contain issue pattern '{expected_pattern}'"
),
"exact_next_action": (
f"Supply a branch_name containing '{expected_pattern}', e.g., 'fix/issue-{issue_number}-...'"
),
}
worktree_name = target_branch.replace("/", "-")
target_worktree = (worktree_path or "").strip()
if not target_worktree:
target_worktree = os.path.join(root, "branches", worktree_name)
target_worktree = os.path.realpath(os.path.abspath(target_worktree))
key = (idempotency_key or "").strip()
if not key:
key = derive_default_idempotency_key(
remote=remote,
org=org,
repo=repo,
issue_number=issue_number,
assignment_id=assignment_id,
lease_id=lease_id,
)
# Review #531 Finding 4: never embed unverified caller-supplied IDs.
id_block = _verify_assignment_and_lease_ids(
assignment_id=assignment_id,
lease_id=lease_id,
issue_number=issue_number,
owner_session=session,
remote=remote,
org=org,
repo=repo,
)
if id_block is not None:
return id_block
# Acquire cross-process file lock scoped to the idempotency key / transition identity
with BootstrapTransitionLock(key, journal_dir=lock_dir):
# Idempotency check
existing = load_phase_journal(key, journal_dir=lock_dir)
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": (
"Call gitea_whoami, then gitea_resolve_task_capability(task='work_issue') "
"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": identity,
"active_profile": profile,
"owner_session": 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, journal_dir=lock_dir)
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, journal_dir=lock_dir)
if dry_run:
return {
"success": True,
"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": live_master_sha,
"phase_journal": journal,
"exact_next_action": "Run without dry_run=True to execute bootstrap.",
}
# Phase 2: BRANCH_CONFIRMED
was_branch_created_previously = journal["artifacts_created"].get("branch_created", False)
pending_branch = (journal.get("pending_creations") or {}).get("branch_name")
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,
)
# F-8 / review #531 Finding 3: require the existing branch head to
# *contain* live master (is-ancestor) or equal it. Sharing any
# historical merge-base is not enough — that would accept stale or
# diverged branches that merely share history with master.
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]}) "
f"does not contain live master ({live_master_sha[:12]})"
)
save_phase_journal(journal, journal_dir=lock_dir)
return {
"success": False,
"reason_code": "incompatible_existing_branch",
"message": (
f"Existing branch '{target_branch}' HEAD ({branch_head[:12]}) "
f"does not contain live master ({live_master_sha[:12]}). "
"Stale or diverged branches are refused (fail closed)."
),
"exact_next_action": (
"Update the branch by merging current master (no rebase/"
"force-push), or choose a branch that already contains master."
),
}
# Preserve creation provenance monotonically across interruption and replay
if was_branch_created_previously or pending_branch == target_branch:
journal["artifacts_created"]["branch_created"] = True
else:
journal["artifacts_created"]["branch_created"] = False
else:
# Persist creation intent/provenance to disk BEFORE executing external mutation
journal.setdefault("pending_creations", {})["branch_name"] = target_branch
journal["artifacts_created"]["branch_created"] = True
save_phase_journal(journal, journal_dir=lock_dir)
# 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["artifacts_created"]["branch_created"] = False
journal.get("pending_creations", {}).pop("branch_name", None)
journal["failure_reason"] = (
f"failed to create git branch '{target_branch}': {create_res.stderr.strip()}"
)
save_phase_journal(journal, journal_dir=lock_dir)
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["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, journal_dir=lock_dir)
# 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, journal_dir=lock_dir)
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-...'"
),
}
was_dir_created_previously = journal["artifacts_created"].get("worktree_dir_created", False)
was_registered_previously = journal["artifacts_created"].get("worktree_registered", False)
pending_wt = (journal.get("pending_creations") or {}).get("worktree_path")
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, journal_dir=lock_dir)
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, journal_dir=lock_dir)
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."
),
}
if was_dir_created_previously or pending_wt == target_worktree:
journal["artifacts_created"]["worktree_dir_created"] = True
journal["artifacts_created"]["worktree_registered"] = True
else:
journal["artifacts_created"]["worktree_dir_created"] = False
journal["artifacts_created"]["worktree_registered"] = False
else:
# Persist creation intent/provenance to disk BEFORE executing external worktree add mutation
journal.setdefault("pending_creations", {})["worktree_path"] = target_worktree
journal["artifacts_created"]["worktree_dir_created"] = True
journal["artifacts_created"]["worktree_registered"] = True
save_phase_journal(journal, journal_dir=lock_dir)
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["artifacts_created"]["worktree_dir_created"] = False
journal["artifacts_created"]["worktree_registered"] = False
journal.get("pending_creations", {}).pop("worktree_path", None)
journal["failure_reason"] = (
f"git worktree add failed: {wt_add_res.stderr.strip()}"
)
run_compensating_recovery(journal, root, journal_dir=lock_dir)
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["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, journal_dir=lock_dir)
# 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, journal_dir=lock_dir)
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, journal_dir=lock_dir)
# Phase 6: STATE_ESTABLISHED — Issue Lock Acquisition
#
# #953: this used to hand-build a thinner record — claimant at the top
# level, no work_lease, no lock_provenance, no expiry — which every
# downstream reader then refused. It now builds through the one shared
# canonical contract, so the lock bootstrap writes is the same lock
# gitea_lock_issue writes.
from datetime import datetime, timezone
try:
lock_data = author_lock_contract.build_canonical_issue_lock(
issue_number=issue_number,
branch_name=target_branch,
worktree_path=target_worktree,
remote=remote,
org=org or "Scaled-Tech-Consulting",
repo=repo or "Gitea-Tools",
identity=identity,
profile=profile,
tool="gitea_bootstrap_author_issue_worktree",
source=author_lock_contract.SOURCE_BOOTSTRAP,
owner_session=session,
assignment_id=assignment_id,
lease_id=lease_id,
expected_base_sha=live_master_sha,
)
lock_data["created_at"] = datetime.now(timezone.utc).isoformat()
journal.setdefault("pending_creations", {})["lock"] = True
journal["artifacts_created"]["lock_created"] = True
save_phase_journal(journal, journal_dir=lock_dir)
lock_res = issue_lock_store.bind_session_lock(lock_data, lock_dir=lock_dir)
except Exception as exc:
journal["artifacts_created"]["lock_created"] = False
journal.get("pending_creations", {}).pop("lock", None)
journal["failure_reason"] = f"issue lock binding failed: {exc}"
run_compensating_recovery(journal, root, journal_dir=lock_dir)
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.",
}
# ── #953 AC7: verify the lock that was actually written ──
# Reporting "lock_created: true" and then directing the author to
# implement is what produced the unrecoverable state: by the time any
# reader refused the lock, the branch already carried commits and every
# sanctioned recovery path had become ineligible. The lock is therefore
# read back from disk and structurally verified *before* this function
# can report success, and a partial lock fails closed here — while the
# branch is still base-equivalent and recovery is still cheap.
written_lock = issue_lock_store.read_lock_file(lock_res)
contract = author_lock_contract.assess_lock_contract(written_lock)
if not contract["canonical"]:
journal["failure_reason"] = author_lock_contract.format_contract_refusal(
contract
)
compensation = run_compensating_recovery(
journal, root, journal_dir=lock_dir
)
# AC5/AC15: the recommendation must describe the state compensation
# actually left, not the state that provoked it.
# ``run_compensating_recovery`` has by now released the lock, removed
# the worktree, and deleted the branch, so recommending
# incomplete-lock recovery for those exact artifacts would refuse
# twice over. Observe what survived and answer for that.
post_state = author_lock_contract.assess_post_compensation_state(
compensation,
lock_present=bool(lock_res) and os.path.exists(lock_res),
worktree_present=os.path.isdir(target_worktree),
branch_present=_branch_exists(root, target_branch),
)
return {
"success": False,
"reason_code": "incomplete_issue_lock_contract",
"message": author_lock_contract.format_contract_refusal(contract),
"issue_number": issue_number,
"branch_name": target_branch,
"worktree_path": target_worktree,
"lock_state": lock_res,
"lock_contract": contract,
"missing_fields": contract["missing_fields"],
"implementation_allowed": False,
"compensating_recovery": compensation,
"post_compensation_state": post_state,
# AC15: never strand a branch or worktree without a structured
# recovery recommendation — and never name an artifact the
# rollback has already deleted.
"exact_next_action": author_lock_contract.post_compensation_action(
post_state,
issue_number=issue_number,
branch_name=target_branch,
worktree_path=target_worktree,
missing_fields=contract["missing_fields"],
),
"phase_journal": journal,
}
journal["phases"][PHASE_6_STATE_ESTABLISHED] = {
"status": "completed",
"lock": lock_res,
"lock_contract": contract["contract"],
}
journal["phases"][PHASE_7_TRANSITION_COMPLETED] = {
"status": "completed",
}
journal["current_phase"] = PHASE_7_TRANSITION_COMPLETED
journal["completed"] = True
save_phase_journal(journal, journal_dir=lock_dir)
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,
"lock_contract": contract,
# #953 AC6: the canonical ownership token for this claim. Never null
# on a successful bootstrap — it is the fencing token every
# subsequent heartbeat and renewal is checked against.
"task_session_id": contract["task_session_id"],
"implementation_allowed": True,
"phase_journal": journal,
# #953 AC5: executable under the state actually returned. The lock
# has been read back and verified canonical, so proceeding to
# implementation is genuinely the correct next step here — which is
# exactly what the old unconditional wording could not promise.
"exact_next_action": author_lock_contract.recommended_action(contract),
}