feat: surface explicit adoption proof in gitea_lock_issue response (Closes #477)
When gitea_lock_issue adopts an existing own branch, the live tool response now carries explicit, citable adoption-proof fields so #473-style recovery sessions can quote the lock output directly instead of inferring adoption from separate offline checks. - issue_lock_adoption.py: add DECISION_LABELS + decision_label()/ safe_next_action() helpers; extend build_adoption_proof() with adoption_decision, adopted, adopted_branch, adopted_branch_head, matcher_summary (boundary-safe reason), competing_branch_check, and safe_next_action for all outcomes; add build_non_adoption_lock_proof() for adoption-free NO_MATCH metadata. - gitea_mcp_server.py: attach adoption-free adoption_check block to normal (non-adopt) lock responses so they cannot be misread as claiming adoption. - docs/llm-workflow-runbooks.md: document the adoption/adoption_check response blocks and instruct recovery reports to cite them directly. - tests: explicit-field proof for ADOPT/BLOCK_COMPETING/NO_MATCH, substring-collision boundary safety (issue-42 vs issue-420), non-adoption proof, and MCP-level live-response assertions. BLOCK_COMPETING lock attempts still fail closed (raise); no raw API fallback, branch deletion, force-push, or manual lock seeding introduced. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -20,6 +20,43 @@ ADOPT = "adopt_existing_branch"
|
||||
BLOCK_COMPETING = "block_competing_branch"
|
||||
NO_MATCH = "no_matching_branch"
|
||||
|
||||
# Citable decision labels aligned with the ``assess_own_branch_adoption``
|
||||
# outcomes, surfaced verbatim in the live ``gitea_lock_issue`` response so
|
||||
# recovery reports (#473-style) can quote the lock tool output directly
|
||||
# instead of inferring adoption from separate offline checks (#477).
|
||||
DECISION_LABELS = {
|
||||
ADOPT: "ADOPT",
|
||||
BLOCK_COMPETING: "BLOCK_COMPETING",
|
||||
NO_MATCH: "NO_MATCH",
|
||||
}
|
||||
|
||||
_SAFE_NEXT_ACTIONS = {
|
||||
ADOPT: (
|
||||
"Own existing branch adopted for lock recovery; proceed to "
|
||||
"gitea_create_pr for this issue and cite this adoption proof."
|
||||
),
|
||||
BLOCK_COMPETING: (
|
||||
"Competing same-issue branch(es) exist; resolve branch ownership "
|
||||
"before locking. No adoption performed (fail closed)."
|
||||
),
|
||||
NO_MATCH: (
|
||||
"No existing branch carries this issue marker; normal lock path "
|
||||
"applied. No adoption performed."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def decision_label(outcome: str) -> str:
|
||||
"""Map an ``assess_own_branch_adoption`` outcome to its citable label."""
|
||||
return DECISION_LABELS.get(outcome, "UNKNOWN")
|
||||
|
||||
|
||||
def safe_next_action(outcome: str) -> str:
|
||||
"""Return the safe next action string for an adoption *outcome*."""
|
||||
return _SAFE_NEXT_ACTIONS.get(
|
||||
outcome, "Unknown adoption outcome; treat as fail closed."
|
||||
)
|
||||
|
||||
|
||||
def _branch_name(entry) -> str:
|
||||
if isinstance(entry, dict):
|
||||
@@ -128,6 +165,42 @@ def assess_own_branch_adoption(
|
||||
}
|
||||
|
||||
|
||||
def _matcher_summary(issue_number: int, assessment: dict) -> str:
|
||||
"""Explain, citably, why the assessed branch did or did not qualify.
|
||||
|
||||
Names the numeric word-boundary rule so reports can show that
|
||||
``issue-42`` was not matched inside ``issue-420`` (#440 / #477 AC3).
|
||||
"""
|
||||
outcome = assessment.get("outcome")
|
||||
matched = assessment.get("matched_branch")
|
||||
competing = assessment.get("competing_branches") or []
|
||||
if outcome == ADOPT and matched:
|
||||
return (
|
||||
f"branch '{matched}' exactly matches the issue-{int(issue_number)} "
|
||||
f"marker (numeric word-boundary; 'issue-{int(issue_number)}' is not "
|
||||
f"matched inside 'issue-{int(issue_number)}0')"
|
||||
)
|
||||
if outcome == BLOCK_COMPETING:
|
||||
return (
|
||||
f"competing same-issue branch(es) {competing} carry the "
|
||||
f"issue-{int(issue_number)} marker but are not the requested "
|
||||
f"branch; ownership is ambiguous (fail closed)"
|
||||
)
|
||||
return (
|
||||
f"no existing branch carries the issue-{int(issue_number)} marker "
|
||||
f"under the numeric word-boundary rule"
|
||||
)
|
||||
|
||||
|
||||
def _competing_branch_check(assessment: dict) -> dict:
|
||||
"""Structured competing-branch verdict for the proof block."""
|
||||
competing = list(assessment.get("competing_branches") or [])
|
||||
return {
|
||||
"result": "blocked" if competing else "clear",
|
||||
"competing_branches": competing,
|
||||
}
|
||||
|
||||
|
||||
def build_adoption_proof(
|
||||
*,
|
||||
issue_number: int,
|
||||
@@ -143,7 +216,18 @@ def build_adoption_proof(
|
||||
Requirement #4: adoption results must carry issue number, branch name,
|
||||
branch head commit, adoption reason, no-existing-PR proof, no-competing-
|
||||
live-lock proof, and lock file path/status.
|
||||
|
||||
#477: additionally surface explicit, citable adoption-proof fields tied to
|
||||
the ``assess_own_branch_adoption`` outcome (``adoption_decision``,
|
||||
``adopted``, ``adopted_branch``, ``adopted_branch_head``,
|
||||
``matcher_summary``, ``competing_branch_check``, ``safe_next_action``) so a
|
||||
recovery session can quote the live lock response directly. The explicit
|
||||
fields are populated for any outcome; ``adopted_branch`` /
|
||||
``adopted_branch_head`` are set only when the outcome is ADOPT so a
|
||||
non-adoption proof can never be misread as claiming adoption.
|
||||
"""
|
||||
outcome = assessment.get("outcome")
|
||||
adopted = outcome == ADOPT
|
||||
return {
|
||||
"issue_number": issue_number,
|
||||
"branch_name": branch_name,
|
||||
@@ -153,4 +237,36 @@ def build_adoption_proof(
|
||||
"no_competing_live_lock_proof": bool(competing_lock_checked),
|
||||
"lock_file_path": lock_file_path,
|
||||
"lock_file_status": lock_file_status,
|
||||
# Explicit citable fields (#477).
|
||||
"adoption_decision": decision_label(outcome),
|
||||
"adopted": adopted,
|
||||
"adopted_branch": branch_name if adopted else None,
|
||||
"adopted_branch_head": assessment.get("matched_head_sha") if adopted else None,
|
||||
"matcher_summary": _matcher_summary(issue_number, assessment),
|
||||
"competing_branch_check": _competing_branch_check(assessment),
|
||||
"safe_next_action": safe_next_action(outcome),
|
||||
}
|
||||
|
||||
|
||||
def build_non_adoption_lock_proof(*, issue_number: int, branch_name: str) -> dict:
|
||||
"""Safe, adoption-free proof metadata for a normal (NO_MATCH) lock.
|
||||
|
||||
Requirement #477 AC2: non-adoption lock responses must stay clear and must
|
||||
not imply adoption. This returns explicit ``adopted: False`` metadata with
|
||||
the ``NO_MATCH`` decision so a normal lock response can carry citable proof
|
||||
without ever asserting a branch was adopted.
|
||||
"""
|
||||
return {
|
||||
"issue_number": issue_number,
|
||||
"branch_name": branch_name,
|
||||
"adoption_decision": DECISION_LABELS[NO_MATCH],
|
||||
"adopted": False,
|
||||
"adopted_branch": None,
|
||||
"adopted_branch_head": None,
|
||||
"matcher_summary": (
|
||||
f"no existing branch carries the issue-{int(issue_number)} marker; "
|
||||
f"normal lock path (no adoption)"
|
||||
),
|
||||
"competing_branch_check": {"result": "clear", "competing_branches": []},
|
||||
"safe_next_action": safe_next_action(NO_MATCH),
|
||||
}
|
||||
Reference in New Issue
Block a user