fix: resolve conflicts for PR #417

Merge prgs/master into feat/issue-404-worktree-cleanup-audit and keep both
worktree cleanup audit proof (#404) and reviewer validation cwd proof from master.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
2026-07-08 03:58:41 -04:00
co-authored by Claude Opus 4.8
19 changed files with 1491 additions and 10 deletions
+101 -5
View File
@@ -417,8 +417,13 @@ def record_preflight_check(type_name: str, resolved_role: str | None = None):
def _enforce_branches_only_author_mutation(worktree_path: str | None = None) -> None:
"""#274: author mutations must run from a branches/ session worktree."""
if _preflight_resolved_role == "reviewer":
"""#274: author file/branch mutations must run from a branches/ worktree.
Reviewer and reconciler roles are exempt: reconciler ``close_pr`` is a
Gitea metadata mutation and must not require ``GITEA_AUTHOR_WORKTREE``
(#468).
"""
if _preflight_resolved_role in ("reviewer", "reconciler"):
return
ctx = _resolve_author_mutation_context(worktree_path)
workspace = ctx["workspace_path"]
@@ -535,6 +540,7 @@ import issue_lock_worktree # noqa: E402
import issue_lock_provenance # noqa: E402
import issue_lock_store # noqa: E402
import issue_lock_adoption # noqa: E402
import stacked_pr_support # noqa: E402
import merge_approval_gate # noqa: E402
import already_landed_reconcile # noqa: E402
import author_mutation_worktree # noqa: E402
@@ -1263,6 +1269,16 @@ def gitea_create_issue(
@mcp.tool()
def _list_open_pulls(h: str, o: str, r: str, auth: str) -> list[dict]:
"""Fetch all OPEN pull requests for a repo (used for stacked-base proof, #484)."""
try:
return api_get_all(f"{repo_api_url(h, o, r)}/pulls?state=open", auth) or []
except Exception as exc: # fail closed: no proof of an open dependency PR
raise RuntimeError(
f"Could not list open pull requests to verify stacked base: {exc}"
)
def gitea_lock_issue(
issue_number: int,
branch_name: str,
@@ -1271,6 +1287,8 @@ def gitea_lock_issue(
org: str | None = None,
repo: str | None = None,
worktree_path: str | None = None,
stacked_base_branch: str | None = None,
stacked_base_pr: int | None = None,
) -> dict:
"""Lock exactly one Gitea issue and its branch name to ensure durable tracking.
@@ -1283,6 +1301,15 @@ def gitea_lock_issue(
repo: Override Repo.
worktree_path: Author scratch-clone path to validate (defaults to
GITEA_AUTHOR_WORKTREE or the MCP server project root).
stacked_base_branch: Opt-in. Declare a non-master base branch for a
*stacked* PR (a PR based on another unmerged PR's branch). Normal work
leaves this ``None`` and stays master-equivalent. When set, the
worktree may be base-equivalent to this branch instead of
master/main/dev, and the approved base is recorded on the lock (#484).
stacked_base_pr: Required when ``stacked_base_branch`` is set. The number
of the OPEN pull request that owns the stacked base branch. The lock
fails closed unless this open PR exists and owns that branch, so
arbitrary or stale branches cannot be used as stacked bases.
"""
# 1. Enforce branch name includes issue number
expected_pattern = f"issue-{issue_number}"
@@ -1310,7 +1337,31 @@ def gitea_lock_issue(
if active_lease_block:
raise RuntimeError(active_lease_block)
git_state = issue_lock_worktree.read_worktree_git_state(resolved_worktree)
# ── Stacked-PR base declaration (opt-in, #484) ──
# Normal work leaves stacked_base_branch None → master-equivalent path.
# A declared stacked base must be proven to own an OPEN PR before it can
# anchor base-equivalence; this never bypasses the lock.
stacked_extra_bases: tuple[str, ...] = ()
stacked_approved: dict | None = None
if stacked_base_branch:
stacked_assessment = stacked_pr_support.assess_stacked_base_declaration(
stacked_base_branch=stacked_base_branch,
stacked_base_pr=stacked_base_pr,
open_prs=_list_open_pulls(h, o, r, _auth(h)),
)
if stacked_assessment["block"]:
raise RuntimeError(
"; ".join(stacked_assessment["reasons"]) + " (fail closed)"
)
stacked_approved = stacked_assessment["approved"]
stacked_extra_bases = (stacked_approved["branch"],)
if stacked_extra_bases:
git_state = issue_lock_worktree.read_worktree_git_state(
resolved_worktree, extra_bases=stacked_extra_bases
)
else:
git_state = issue_lock_worktree.read_worktree_git_state(resolved_worktree)
verify_preflight_purity(remote, worktree_path=resolved_worktree)
lock_assessment = issue_lock_worktree.assess_issue_lock_worktree(
worktree_path=resolved_worktree,
@@ -1383,6 +1434,8 @@ def gitea_lock_issue(
claimant=work_lease.get("claimant"),
),
}
if stacked_approved:
data["approved_stacked_base"] = stacked_approved
lock_file_path = _save_issue_lock(data)
lock_record = issue_lock_store.read_lock_file(lock_file_path) or data
@@ -1416,6 +1469,13 @@ def gitea_lock_issue(
"lock_freshness": freshness,
"lock_proof": lock_proof,
}
if stacked_approved:
result["approved_stacked_base"] = stacked_approved
result["message"] = (
f"Successfully locked issue #{issue_number} to branch '{branch_name}' "
f"as a STACKED PR on base '{stacked_approved['branch']}' "
f"(open PR #{stacked_approved['pr_number']}); fail-closed check complete."
)
if adoption["adopt"]:
result["adoption"] = issue_lock_adoption.build_adoption_proof(
issue_number=issue_number,
@@ -1430,6 +1490,14 @@ def gitea_lock_issue(
f"Adopted existing branch '{branch_name}' and locked issue "
f"#{issue_number} for recovery (fail-closed check complete)."
)
else:
# #477 AC2: normal (no-adoption) lock responses carry explicit,
# adoption-free proof metadata so they stay clear and cannot be
# misread as claiming a branch was adopted.
result["adoption_check"] = issue_lock_adoption.build_non_adoption_lock_proof(
issue_number=issue_number,
branch_name=branch_name,
)
if agent_artifacts:
result["warnings"] = [
"Agent temp artifacts at repo root (delete before implementation): "
@@ -1572,6 +1640,24 @@ def gitea_create_pr(
f"PR title or body must contain 'Closes #{locked_issue}' or 'Fixes #{locked_issue}' exactly to ensure durable tracking (fail closed)"
)
# ── Stacked-PR base validation (#484) ──
# Normal base branches (master/main/dev) pass unchanged. A non-base target is
# allowed only when it matches the lock's approved stacked base, that base still
# has an open PR, and the body documents the stack. This never bypasses the lock.
base_open_prs = (
[]
if stacked_pr_support.is_base_branch(base)
else _list_open_pulls(h, o, r, _auth(h))
)
base_check = stacked_pr_support.assess_create_pr_base(
base=base,
approved_stacked_base=lock_data.get("approved_stacked_base"),
body=body,
open_prs=base_open_prs,
)
if base_check["block"]:
raise ValueError("; ".join(base_check["reasons"]) + " (fail closed)")
duplicate_block = _enforce_locked_issue_duplicate_recheck(
remote,
issue_work_duplicate_gate.PHASE_CREATE_PR,
@@ -2516,7 +2602,12 @@ def _list_pr_lease_comments(
h, o, r = _resolve(remote, host, org, repo)
auth = _auth(h)
api = f"{repo_api_url(h, o, r)}/issues/{pr_number}/comments"
comments = api_request("GET", api, auth) or []
comments = api_request("GET", api, auth)
# Fail safe to no lease comments when the API returns a non-list payload
# (e.g. an error object such as an HTTP 401 body): lease state can only be
# proven from real comment entries, never inferred from an error shape (#485).
if not isinstance(comments, list):
return []
return list(comments[:limit])
@@ -5173,7 +5264,12 @@ def gitea_list_issue_comments(
h, o, r = _resolve(remote, host, org, repo)
auth = _auth(h)
api = f"{repo_api_url(h, o, r)}/issues/{issue_number}/comments"
comments = api_request("GET", api, auth) or []
comments = api_request("GET", api, auth)
# Fail safe to no comments when the API returns a non-list payload (e.g. an
# error object such as an HTTP 401 body) so listing never crashes on a
# malformed response (#485).
if not isinstance(comments, list):
comments = []
reveal = _reveal_endpoints()
out = []
for c in comments[:limit]: