Files
Gitea-Tools/author_lock_contract.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

624 lines
25 KiB
Python

"""One canonical author issue-lock contract shared by every writer (#953).
Before this module, ``gitea_lock_issue`` and
``gitea_bootstrap_author_issue_worktree`` each wrote their own lock record.
``gitea_lock_issue`` wrote the canonical shape — ``work_lease`` carrying the
claimant plus a sanctioned ``lock_provenance`` — while bootstrap wrote a thinner
record with the claimant at the lock top level, ``lease_id: null``, and no
``work_lease``, ``lock_provenance``, or expiry at all.
Every downstream reader was written against the canonical shape, so a lock that
bootstrap reported as successfully created was simultaneously:
* un-heartbeatable — the ownership check read the claimant only from
``work_lease.claimant``;
* un-renewable — expiry is read only from ``work_lease.expires_at``, so a
missing lease read as "never expires", and #760 exact-owner renewal only ever
assesses an *expired* lease;
* un-re-lockable — the branch had by then advanced past its base;
* and rejected by the #447 create-PR provenance guard.
Each of those gates is individually correct. The defect was that two writers
disagreed about what a lock *is*. This module is the single definition, and both
writers now build through it.
Nothing here weakens a guard. ``build_sanctioned_lock_provenance`` remains the
only provenance source, provenance is never accepted from a caller, and the
#447 guard is untouched — this module simply makes bootstrap satisfy it.
"""
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from typing import Any, Mapping
import issue_lock_provenance
import issue_lock_store
import lease_policy
# Bootstrap writes through the same sanctioned source as gitea_lock_issue: the
# lock it produces *is* a canonical lock, not a second dialect that readers must
# learn. Adding a distinct source would have required widening
# SANCTIONED_LOCK_SOURCES, which is exactly the #447 weakening this issue's
# safety requirements forbid.
SOURCE_BOOTSTRAP = issue_lock_provenance.SOURCE_LOCK_ISSUE
# Recovery of an incomplete bootstrap lock (#953 AC8-AC11) deliberately writes
# through SOURCE_LOCK_ISSUE too, and records its distinctness in
# ``lock_provenance.written_by_tool`` plus the ``bootstrap_lock_recovery``
# transition block instead. There is no distinct recovery *source* constant, for
# the same reason bootstrap has none: minting one would require widening
# SANCTIONED_LOCK_SOURCES, which the #447 safety requirements forbid.
#: Top-level keys every canonical author issue lock must carry.
REQUIRED_LOCK_FIELDS: tuple[str, ...] = (
"remote",
"org",
"repo",
"issue_number",
"branch_name",
"worktree_path",
"work_lease",
"lock_provenance",
)
#: Keys every canonical ``work_lease`` must carry.
REQUIRED_WORK_LEASE_FIELDS: tuple[str, ...] = (
"operation_type",
"issue_number",
"branch",
"worktree_path",
"claimant",
"created_at",
"expires_at",
"last_heartbeat_at",
"task_session_id",
"lifecycle_version",
)
# ── Explicit expiration states (AC12) ──
# The bug this replaces: a lock with no recorded expiry produced
# ``is_lease_expired() -> False``, which reads as "not yet expired" and made the
# lock permanently non-expiring *and* permanently ineligible for the renewal
# path, which only ever assesses an expired lease. "Absent" and "in the future"
# are different facts and are now named differently.
EXPIRATION_RECORDED = "recorded"
EXPIRATION_MISSING = "missing"
EXPIRATION_UNPARSEABLE = "unparseable"
#: Structural verdicts returned by :func:`assess_lock_contract`.
CONTRACT_CANONICAL = "canonical"
CONTRACT_INCOMPLETE = "incomplete"
CONTRACT_LEGACY = "legacy"
CONTRACT_ABSENT = "absent"
def _text(value: Any) -> str:
return str(value or "").strip()
def now_utc() -> datetime:
return datetime.now(timezone.utc)
def format_timestamp(value: datetime) -> str:
"""Serialize in the durable ``...Z`` form already used on disk."""
return (
value.astimezone(timezone.utc)
.replace(microsecond=0)
.isoformat()
.replace("+00:00", "Z")
)
def lock_claimant(lock: Mapping[str, Any] | None) -> dict[str, str]:
"""Read the claimant from either canonical or legacy placement.
``work_lease.claimant`` is canonical and is preferred. A top-level
``claimant`` is the legacy/bootstrap placement and is accepted as a
fallback (AC14) — three separate readers already disagreed about this
(``issue_lock_store``, ``issue_lock_renewal``, ``issue_lock_recovery``),
which is why it now lives in one place.
Reading a legacy placement is *not* a widening: every caller still compares
the values it returns against server-resolved identity and profile. This
only decides where to look, never whether ownership is proven.
Delegates to ``issue_lock_store.lock_claimant`` rather than reimplementing
the rule. A second copy here would be a fourth reader that could drift from
the other three, which is the exact failure #953 exists to end. It lives in
the store because ``author_lock_contract`` imports the store, so defining it
here would make that import circular.
"""
recorded = issue_lock_store.lock_claimant(dict(lock) if isinstance(lock, Mapping) else None)
return {
"username": _text(recorded.get("username")),
"profile": _text(recorded.get("profile")),
}
def claimant_placement(lock: Mapping[str, Any] | None) -> str:
"""Where the claimant was found: ``work_lease``, ``top_level``, or ``absent``."""
if not isinstance(lock, Mapping):
return "absent"
lease = lock.get("work_lease")
if isinstance(lease, Mapping) and isinstance(lease.get("claimant"), Mapping):
return "work_lease"
if isinstance(lock.get("claimant"), Mapping):
return "top_level"
return "absent"
def build_claimant(*, username: str | None, profile: str | None) -> dict[str, str]:
"""Build the canonical claimant pair from server-resolved values."""
return {"username": _text(username), "profile": _text(profile)}
def build_author_issue_work_lease(
*,
issue_number: int,
branch_name: str,
worktree_path: str,
claimant: Mapping[str, Any],
task_session_id: str | None = None,
created: datetime | None = None,
) -> dict[str, Any]:
"""Build the canonical author ``work_lease``.
The single definition behind both writers. The TTL comes from the central
policy rather than a literal, and the window slides from the last valid
heartbeat (#790), so an abandoned task releases its claim within one TTL.
"""
started = created or now_utc()
policy = lease_policy.policy_for(lease_policy.TASK_CLASS_AUTHOR_ISSUE_WORK)
expires = started + timedelta(minutes=policy.initial_ttl_minutes)
session_id = _text(task_session_id) or issue_lock_store.mint_task_session_id(
issue_lock_store.AUTHOR_ISSUE_WORK_LEASE
)
return {
"operation_type": issue_lock_store.AUTHOR_ISSUE_WORK_LEASE,
"issue_number": int(issue_number),
"pr_number": None,
"branch": branch_name,
"worktree_path": worktree_path,
"claimant": dict(claimant),
"created_at": format_timestamp(started),
"expires_at": format_timestamp(expires),
"last_heartbeat_at": format_timestamp(started),
# #790 AC-N1: the ownership key for this task, distinct from the
# recorded PID, which is the shared daemon and identifies no task.
"task_session_id": session_id,
# #790 AC-N8: the explicit lifecycle marker. Its absence — never a
# timestamp comparison — is what makes a lock legacy.
"lifecycle_version": lease_policy.LIFECYCLE_HEARTBEAT_V1,
"heartbeat_count": 1,
}
def build_canonical_issue_lock(
*,
issue_number: int,
branch_name: str,
worktree_path: str,
remote: str,
org: str,
repo: str,
identity: str | None,
profile: str | None,
tool: str,
source: str = issue_lock_provenance.SOURCE_LOCK_ISSUE,
owner_session: str | None = None,
assignment_id: str | None = None,
lease_id: str | None = None,
expected_base_sha: str | None = None,
task_session_id: str | None = None,
created: datetime | None = None,
) -> dict[str, Any]:
"""Build a complete canonical lock record.
``tool`` and ``source`` are server-supplied. There is deliberately no
parameter through which a caller could inject provenance: the #953 safety
requirements forbid caller-manufactured provenance, so provenance is always
minted here from ``build_sanctioned_lock_provenance``.
"""
claimant = build_claimant(username=identity, profile=profile)
work_lease = build_author_issue_work_lease(
issue_number=issue_number,
branch_name=branch_name,
worktree_path=worktree_path,
claimant=claimant,
task_session_id=task_session_id,
created=created,
)
record: dict[str, Any] = {
"remote": remote,
"org": org,
"repo": repo,
"issue_number": int(issue_number),
"branch": branch_name,
"branch_name": branch_name,
"worktree_path": worktree_path,
"work_lease": work_lease,
"lock_provenance": issue_lock_provenance.build_sanctioned_lock_provenance(
tool=tool,
source=source,
claimant=claimant,
),
}
if owner_session is not None:
record["owner_session"] = owner_session
if assignment_id is not None:
record["assignment_id"] = assignment_id
# #953 AC6: a null lease id is recorded only when no workflow lease was
# allocated for this bootstrap. The task-session identifier in the
# work_lease is what downstream ownership checks fence on, and it is never
# null on a canonical lock.
if lease_id is not None:
record["lease_id"] = lease_id
if expected_base_sha is not None:
record["expected_base_sha"] = expected_base_sha
return record
def expiration_state(lock: Mapping[str, Any] | None) -> dict[str, Any]:
"""Classify a lock's recorded expiry explicitly (AC12).
Distinguishes "no expiry was ever recorded" from "an expiry was recorded
and is still in the future". Collapsing those two into a single ``False``
from ``is_lease_expired`` is what let a malformed lock be treated as
permanently live and simultaneously never renewable.
"""
if not isinstance(lock, Mapping):
return {"state": EXPIRATION_MISSING, "expires_at": None, "expired": None}
lease = lock.get("work_lease")
raw = lease.get("expires_at") if isinstance(lease, Mapping) else None
text = _text(raw)
if not text:
return {"state": EXPIRATION_MISSING, "expires_at": None, "expired": None}
try:
parsed = datetime.fromisoformat(text.replace("Z", "+00:00")).astimezone(
timezone.utc
)
except ValueError:
return {"state": EXPIRATION_UNPARSEABLE, "expires_at": text, "expired": None}
return {
"state": EXPIRATION_RECORDED,
"expires_at": text,
"expired": parsed <= now_utc(),
}
def missing_contract_fields(lock: Mapping[str, Any] | None) -> list[str]:
"""Name every canonical field a lock does not carry (AC7)."""
if not isinstance(lock, Mapping):
return ["<no lock record>"]
missing: list[str] = []
for field in REQUIRED_LOCK_FIELDS:
value = lock.get(field)
if value is None or (isinstance(value, str) and not value.strip()):
missing.append(field)
lease = lock.get("work_lease")
if not isinstance(lease, Mapping):
if "work_lease" not in missing:
missing.append("work_lease")
else:
for field in REQUIRED_WORK_LEASE_FIELDS:
value = lease.get(field)
if value is None or (isinstance(value, str) and not value.strip()):
missing.append(f"work_lease.{field}")
provenance = lock.get("lock_provenance")
if isinstance(provenance, Mapping):
if (
_text(provenance.get("source"))
not in issue_lock_provenance.SANCTIONED_LOCK_SOURCES
):
missing.append("lock_provenance.source (not sanctioned)")
if not _text(provenance.get("written_by_tool")):
missing.append("lock_provenance.written_by_tool")
claimant = lock_claimant(lock)
if not claimant["username"]:
missing.append("claimant.username")
if not claimant["profile"]:
missing.append("claimant.profile")
return missing
def assess_lock_contract(lock: Mapping[str, Any] | None) -> dict[str, Any]:
"""Structural, read-only verdict on a durable lock record (AC7, AC16).
Pure inspection: it reads the record it is handed and mutates nothing —
no lock, lease, branch, worktree, issue, or PR. Callers use it both to
verify a lock they just wrote and to report on one they found.
"""
if not isinstance(lock, Mapping) or not lock:
return {
"contract": CONTRACT_ABSENT,
"canonical": False,
"missing_fields": ["<no lock record>"],
"claimant": {"username": "", "profile": ""},
"claimant_placement": "absent",
"expiration": {
"state": EXPIRATION_MISSING,
"expires_at": None,
"expired": None,
},
"heartbeatable": False,
"create_pr_eligible": False,
"lock_generation": None,
"task_session_id": None,
"reasons": ["no durable lock record"],
}
missing = missing_contract_fields(lock)
claimant = lock_claimant(lock)
placement = claimant_placement(lock)
expiration = expiration_state(lock)
provenance_check = issue_lock_provenance.assess_lock_file_for_create_pr(dict(lock))
# Canonical means: every required field present, the claimant in the
# canonical placement, an expiry actually recorded, and the untouched #447
# guard satisfied.
canonical = (
not missing
and placement == "work_lease"
and expiration["state"] == EXPIRATION_RECORDED
and bool(provenance_check.get("proven"))
)
if canonical:
contract = CONTRACT_CANONICAL
elif placement == "top_level" and claimant["username"] and claimant["profile"]:
contract = CONTRACT_LEGACY
else:
contract = CONTRACT_INCOMPLETE
reasons: list[str] = []
if missing:
reasons.append("missing canonical fields: " + ", ".join(missing))
if placement == "top_level":
reasons.append(
"claimant recorded at the lock top level rather than in work_lease "
"(legacy/bootstrap placement)"
)
if expiration["state"] == EXPIRATION_MISSING:
reasons.append(
"no expiration recorded; the lock is neither expirable nor renewable "
"until it is upgraded"
)
elif expiration["state"] == EXPIRATION_UNPARSEABLE:
reasons.append(f"unparseable expires_at '{expiration['expires_at']}'")
if provenance_check.get("block"):
reasons.extend(provenance_check.get("reasons") or [])
# Heartbeat needs the claimant pair (from either placement, post-fix) plus a
# task-session identifier to fence on.
lease = lock.get("work_lease")
task_session_id = (
_text(lease.get("task_session_id")) if isinstance(lease, Mapping) else ""
)
heartbeatable = bool(
claimant["username"] and claimant["profile"] and task_session_id
)
return {
"contract": contract,
"canonical": canonical,
"missing_fields": missing,
"claimant": claimant,
"claimant_placement": placement,
"expiration": expiration,
"heartbeatable": heartbeatable,
"create_pr_eligible": bool(provenance_check.get("proven")),
"lock_generation": lock.get("lock_generation"),
"task_session_id": task_session_id or None,
"reasons": reasons,
}
def format_contract_refusal(assessment: Mapping[str, Any]) -> str:
"""Human-readable refusal naming exactly what the lock is missing."""
missing = ", ".join(assessment.get("missing_fields") or []) or "unknown fields"
return (
"Issue lock contract incomplete (#953): "
f"{missing}. The lock cannot be heartbeated, renewed, or accepted by "
"gitea_create_pr in this state (fail closed)"
)
def recommended_action(assessment: Mapping[str, Any]) -> str:
"""The one executable next step for a lock in this state (AC5, AC15)."""
contract = assessment.get("contract")
if contract == CONTRACT_CANONICAL:
return (
"Lock is canonical. Call gitea_whoami, then "
"gitea_resolve_task_capability(task='work_issue'), then proceed with "
"author implementation in the bootstrapped worktree."
)
if contract == CONTRACT_ABSENT:
return (
"No durable lock exists. Call gitea_lock_issue for this issue and "
"branch before writing any implementation bytes."
)
return (
"Do not begin implementation. Call "
"gitea_recover_incomplete_bootstrap_lock for this exact issue, branch, "
"and worktree to upgrade the lock to the canonical contract, or "
"gitea_lock_issue while the worktree is still base-equivalent."
)
# ── Post-compensation recovery guidance (#953 AC5/AC15, review 632 F2) ──
#
# ``recommended_action`` above answers "what can be done about a lock in this
# shape". That is the wrong question on the bootstrap AC7 refusal path, because
# ``run_compensating_recovery`` has already run by the time the answer is
# reported: it releases the lock, removes the worktree when clean — which it
# always is there, no implementation bytes having been written — and deletes the
# created branch. Recommending incomplete-lock recovery for those artifacts
# hands the author two refusals in a row (``no_durable_lock``, then
# ``worktree_invalid``) for a state that a plain bootstrap retry would fix. The
# advice must describe the state that actually *remains*.
#: Compensation removed every artifact this transition created.
CLEANUP_COMPLETE = "complete"
#: Compensation removed some artifacts; others survive and are still actionable.
CLEANUP_PARTIAL = "partial"
#: Compensation itself failed or could not be observed; nothing is provable.
CLEANUP_FAILED = "failed"
def assess_post_compensation_state(
recovery: Mapping[str, Any] | None,
*,
lock_present: bool,
worktree_present: bool,
branch_present: bool,
) -> dict[str, Any]:
"""Classify what survived compensation, from observed durable state.
Pure. The caller observes the filesystem and git; this decides. Observation
is authoritative over the journal's ``rolled_back`` list, which records what
compensation *attempted*: ``run_compensating_recovery`` swallows a failed
lock release and appends nothing, so an absent marker proves nothing either
way. The list is still carried through as corroborating evidence.
The three states are distinct facts, not degrees of the same one:
* ``CLEANUP_COMPLETE`` — compensation ran and nothing it created remains.
* ``CLEANUP_PARTIAL`` — compensation ran and artifacts survive, whether by
design (a worktree dirty at rollback time, a branch carrying commits) or
because a rollback step errored. Either way the surviving set was observed
directly, so it is known and actionable; ``failed_rollback_steps`` records
which cause applies.
* ``CLEANUP_FAILED`` — compensation never ran to completion, so nothing it
would have removed can be assumed removed.
"""
rolled_back = list((recovery or {}).get("rolled_back") or [])
executed = bool((recovery or {}).get("executed"))
failed_steps = [entry for entry in rolled_back if "_failed" in entry]
surviving: list[str] = []
if lock_present:
surviving.append("lock")
if worktree_present:
surviving.append("worktree")
if branch_present:
surviving.append("branch")
if not executed:
state = CLEANUP_FAILED
elif surviving:
state = CLEANUP_PARTIAL
else:
state = CLEANUP_COMPLETE
return {
"cleanup_state": state,
"compensation_executed": executed,
"lock_present": bool(lock_present),
"worktree_present": bool(worktree_present),
"branch_present": bool(branch_present),
"surviving_artifacts": surviving,
"removed_artifacts": [
name
for name, present in (
("lock", lock_present),
("worktree", worktree_present),
("branch", branch_present),
)
if not present
],
"failed_rollback_steps": failed_steps,
"rolled_back": rolled_back,
}
def post_compensation_action(
state: Mapping[str, Any],
*,
issue_number: int,
branch_name: str,
worktree_path: str,
missing_fields: list[str] | None = None,
) -> str:
"""The one executable next step for the state compensation actually left.
Every branch names only artifacts the classification says still exist, so no
recommendation can point at something the rollback deleted.
"""
missing = ", ".join(missing_fields or []) or "the reported missing fields"
cleanup_state = state.get("cleanup_state")
lock_present = bool(state.get("lock_present"))
worktree_present = bool(state.get("worktree_present"))
branch_present = bool(state.get("branch_present"))
if not state.get("compensation_executed"):
# Compensation never ran, so nothing was rolled back and nothing about
# the remaining state was decided. The read-only surface is the only
# action executable under any state.
return (
"Compensating rollback did not complete, so the remaining state is "
f"not proven. Call gitea_inspect_issue_lock_contract for issue "
f"#{issue_number} (read-only) to establish what survives before any "
"further action. Do not retry bootstrap until it is known."
)
prefix = ""
failed_steps = state.get("failed_rollback_steps") or []
if failed_steps:
prefix = (
"Compensating rollback reported a failed step "
f"({', '.join(failed_steps)}); what survives was observed directly "
"and the action below is scoped to exactly that. "
)
if cleanup_state == CLEANUP_COMPLETE:
return (
"Compensating rollback removed the malformed lock, the branch, and "
f"the worktree, so nothing from this attempt remains. Resolve "
f"{missing} and re-run gitea_bootstrap_author_issue_worktree for "
f"issue #{issue_number} from the clean pre-bootstrap state. Do not "
"call gitea_recover_incomplete_bootstrap_lock: there is no lock, "
"branch, or worktree left for it to act on."
)
if lock_present and worktree_present and branch_present:
return prefix + (
"The lock, branch, and worktree all survive. Call "
"gitea_recover_incomplete_bootstrap_lock for issue "
f"#{issue_number}, branch '{branch_name}', and worktree "
f"'{worktree_path}', passing the worktree's current head as "
"expected_head, to upgrade the lock to the canonical contract."
)
if not lock_present and worktree_present and branch_present:
return prefix + (
"The malformed lock was released but the branch and worktree "
"survive. No implementation bytes were written, so the worktree is "
f"still base-equivalent: call gitea_lock_issue for issue "
f"#{issue_number} on branch '{branch_name}' from worktree "
f"'{worktree_path}' to acquire a canonical lock."
)
if lock_present and not worktree_present:
return prefix + (
f"The worktree for issue #{issue_number} is gone but the durable "
"lock survived, so neither gitea_recover_incomplete_bootstrap_lock "
"(it would refuse worktree_invalid) nor gitea_lock_issue (it has no "
"worktree to bind) is executable. Call "
"gitea_inspect_issue_lock_contract for issue "
f"#{issue_number} (read-only) to confirm the surviving lock; it "
"must be released by its recorded owner before bootstrap is "
"retried."
)
# Lock gone, worktree gone, some git artifact left (a branch with commits,
# or a branch this transition did not create).
return prefix + (
"Compensating rollback removed the lock and worktree; branch "
f"'{branch_name}' survives and was not deleted. Call "
f"gitea_inspect_issue_lock_contract for issue #{issue_number} "
"(read-only) to confirm no durable lock remains, then re-run "
"gitea_bootstrap_author_issue_worktree, which will adopt the existing "
"branch rather than recreating it."
)