Merge branch 'master' into fix/issue-790-slice-a-heartbeat-policy

This commit is contained in:
2026-07-23 00:06:18 -05:00
32 changed files with 9006 additions and 144 deletions
+312 -9
View File
@@ -2025,6 +2025,7 @@ import review_quarantine # noqa: E402 # #695 contaminated formal-review quaran
import mcp_daemon_guard # noqa: E402 # #695 native transport provenance
import already_landed_reconcile # noqa: E402
import author_mutation_worktree # noqa: E402
import branch_publish # noqa: E402 # #812 AC20 unpublished-commit publication
import root_checkout_guard # noqa: E402
import workflow_scope_guard # noqa: E402 # #683 production scope / force-on guards
import stable_branch_push_guard # noqa: E402
@@ -9228,6 +9229,253 @@ def gitea_commit_files(
}
def _publication_block(reasons: list[str], **extra) -> dict:
"""Uniform fail-closed shape for publication refusals (#812 AC20)."""
payload = {
"success": False,
"performed": False,
"published": False,
"verified": False,
"outcome": branch_publish.REFUSED,
"reasons": reasons,
# State every record this operation left alone, so a refusal can never
# be misread as a lock mutation (#812 AC23).
"issue_lock_record_mutated": False,
"workflow_lease_touched": False,
}
payload.update(extra)
return payload
@mcp.tool()
def gitea_publish_unpublished_issue_branch(
issue_number: int,
branch_name: str,
worktree_path: str,
expected_head: str,
remote: str = "dadeschools",
host: str | None = None,
org: str | None = None,
repo: str | None = None,
git_remote_name: str | None = None,
expected_file_hashes: dict | None = None,
dry_run: bool = False,
) -> dict:
"""Publish an already-committed, unpublished issue branch (#812 AC20).
Creates the remote head for a branch whose work is *already* a local commit
on a registered, clean worktree, so exact-owner lease renewal
(``issue_lock_renewal``) has the published head its evidence model requires.
This is the one step of the entry point B deadlock that no existing tool can
perform: publication is otherwise lock-derived under #618, and the lock
itself is withheld until a remote head exists.
Not a lock bypass. The branch's **durable issue-lock record must already
name the caller as claimant** ownership is read from the lock file, never
asserted by the caller so this can only publish work the caller already
owns. It refuses a dirty or untracked-carrying worktree, an unregistered
worktree, a changed local HEAD, a remote head that is not an ancestor of the
commit, a competing open PR on another branch for the same issue, and any
declared-hash mismatch. It renews, reclaims, and clears nothing: the durable
issue-lock file and the control-plane workflow lease are both left untouched
(#812 AC23), and the recorded owner pid's liveness is never consulted or
asserted (#812 AC24).
Args:
issue_number: The issue whose recorded claim authorizes publication.
branch_name: Issue branch to publish, ``(fix|feat|docs|chore)/issue-N-``.
worktree_path: Registered worktree holding the commit.
expected_head: Full 40-character SHA of the commit to publish. Required:
publication names the exact commit, and a mismatch fails closed.
remote: Known instance 'dadeschools' or 'prgs'.
host: Override the Gitea host.
org: Override the owner/organization.
repo: Override the repository name.
git_remote_name: Git remote to publish to; defaults to *remote*.
expected_file_hashes: Optional ``{path: sha256}`` verified against the
worktree before publication. Any mismatch or missing file refuses.
dry_run: Report the decision and evidence, mutate nothing.
Returns:
dict with 'success', 'performed', 'published', 'verified', 'outcome',
'remote_head_sha', 'reasons', and 'evidence'.
"""
task = "publish_unpublished_branch"
ok, block_reasons = role_session_router.check_author_mutation_after_reviewer_stop(
task
)
if not ok:
return _publication_block(block_reasons)
blocked = _namespace_mutation_block(task, remote=remote)
if blocked:
return blocked
blocked = _profile_permission_block(
task_capability_map.required_permission(task),
remote=remote, host=host, org=org, repo=repo,
org_explicit=org is not None,
repo_explicit=repo is not None,
)
if blocked:
return blocked
# #815: resolve the caller's worktree *before* preflight and forward it, so
# every workspace-resolution layer behind verify_preflight_purity — including
# the #618 branches-only guard — judges the registered issue worktree this
# publication actually operates on. Resolving it afterwards let preflight
# fall back to the MCP process root, so a daemon rooted at the stable control
# checkout refused a valid explicit worktree before the assessor ever ran.
# A caller supplying nothing usable forwards None and keeps the ordinary
# fail-closed fallback.
explicit_worktree = (worktree_path or "").strip()
workspace = os.path.realpath(os.path.abspath(explicit_worktree or "."))
verify_preflight_purity(
remote,
worktree_path=workspace if explicit_worktree else None,
task=task,
org=org,
repo=repo,
)
h, o, r = _resolve(remote, host, org, repo)
git_remote = (git_remote_name or remote or "").strip()
existing_lock = issue_lock_store.load_issue_lock(
remote=remote, org=o, repo=r, issue_number=int(issue_number)
)
git_state = issue_lock_worktree.read_worktree_git_state(workspace)
registered = author_mutation_worktree.path_in_git_worktree_list(
workspace, PROJECT_ROOT
)
remote_probe = branch_publish.read_remote_branch_head(
workspace, git_remote, branch_name
)
ancestry = None
probe_head = (remote_probe.get("remote_head_sha") or "").strip()
if probe_head and probe_head.lower() != (expected_head or "").strip().lower():
ancestry = branch_publish.read_is_ancestor(workspace, probe_head, expected_head)
# A PR on this very branch is this work's own PR, not a rival claim. Only an
# open PR for the same issue on a *different* branch is a competing claim.
competing: list = []
for pull in _list_open_pulls(h, o, r, _auth(h)):
ref = str((pull.get("head") or {}).get("ref") or "")
if not ref or ref == branch_name:
continue
if issue_lock_adoption.branch_carries_issue_marker(ref, int(issue_number)):
competing.append(pull.get("number"))
observed_hashes = None
if expected_file_hashes:
observed_hashes = branch_publish.hash_worktree_files(
workspace, list(expected_file_hashes.keys())
)
claimant = _work_lease_claimant(h)
assessment = branch_publish.assess_unpublished_commit_publication(
existing_lock,
issue_number=int(issue_number),
branch_name=branch_name,
worktree_path=workspace,
expected_head=expected_head,
remote=remote,
org=o,
repo=r,
identity=claimant.get("username"),
profile=claimant.get("profile"),
worktree_state=git_state,
worktree_registered=registered,
remote_probe=remote_probe,
ancestry=ancestry,
competing_open_prs=competing,
expected_file_hashes=expected_file_hashes,
observed_file_hashes=observed_hashes,
)
if assessment["outcome"] == branch_publish.REFUSED:
return _publication_block(
assessment["reasons"], evidence=assessment["evidence"]
)
if dry_run:
return {
"success": True,
"performed": False,
"published": False,
"verified": False,
"dry_run": True,
"outcome": assessment["outcome"],
"would_publish": assessment["publish_sanctioned"],
"remote_head_sha": assessment["evidence"].get("remote_head_sha"),
"reasons": [],
"evidence": assessment["evidence"],
"issue_lock_record_mutated": False,
"workflow_lease_touched": False,
}
performed = False
if assessment["publish_sanctioned"]:
with _audited(
task, host=h, remote=remote, org=o, repo=r,
target_branch=branch_name,
request_metadata={
"issue_number": int(issue_number),
"expected_head": expected_head,
"git_remote": git_remote,
},
):
push = branch_publish.publish_commit_to_remote_branch(
worktree_path=workspace,
remote_name=git_remote,
branch_name=branch_name,
expected_head=expected_head,
)
if not push.get("success"):
return _publication_block(
push.get("reasons") or ["publication failed"],
evidence=assessment["evidence"],
stderr=push.get("stderr"),
)
performed = True
verification = branch_publish.verify_published_head(
worktree_path=workspace,
remote_name=git_remote,
branch_name=branch_name,
expected_head=expected_head,
)
if not verification.get("verified"):
return _publication_block(
verification.get("reasons") or ["read-after-write verification failed"],
evidence=assessment["evidence"],
performed=performed,
published=performed,
remote_head_sha=verification.get("remote_head_sha"),
)
return {
"success": True,
"performed": performed,
"published": True,
"verified": True,
"outcome": assessment["outcome"],
"remote_head_sha": verification.get("remote_head_sha"),
"reasons": [],
"evidence": assessment["evidence"],
# Publication is the whole of this operation's authority (#812 AC23).
"issue_lock_record_mutated": False,
"workflow_lease_touched": False,
"exact_next_action": (
f"Remote head for '{branch_name}' is now observable. Call "
f"gitea_lock_issue(issue_number={int(issue_number)}, "
f"branch_name='{branch_name}', worktree_path='{workspace}') to renew "
"the exact-owner lease, then gitea_create_pr."
),
}
# Merge methods supported by the Gitea merge API.
_MERGE_METHODS = ("merge", "squash", "rebase")
@@ -12682,10 +12930,39 @@ def _try_auto_switch_for_operation(op: str, host: str | None = None) -> bool:
return False
def _git_default_remote_name(root: str) -> str:
"""First configured git remote name for *root*, defaulting to 'origin'.
Used to resolve the live remote master target for parity (#610). Best
effort: any failure falls back to 'origin' so callers never raise.
"""
try:
res = subprocess.run(
["git", "-C", root, "remote"],
capture_output=True, text=True, check=False,
)
except Exception:
return "origin"
if res.returncode != 0:
return "origin"
names = [n.strip() for n in (res.stdout or "").splitlines() if n.strip()]
return names[0] if names else "origin"
def _current_master_parity() -> dict:
"""Assess this process's code against the on-disk master HEAD (#420)."""
"""Assess this process's code against local and live remote master (#420/#610).
Compares the daemon's startup commit, the on-disk checkout HEAD, and the
live remote master target. A stale daemon relative to live master fails
closed for mutations even when the local checkout HEAD still matches the
startup commit. The live-remote read is best effort: an unresolved live
head leaves read-only diagnostics unblocked but is never mutation-safe.
"""
current_head = master_parity_gate.read_git_head(PROJECT_ROOT)
return master_parity_gate.assess_master_parity(_STARTUP_PARITY, current_head)
live_head = master_parity_gate.read_remote_master_head(
PROJECT_ROOT, remote=_git_default_remote_name(PROJECT_ROOT))
return master_parity_gate.assess_master_parity(
_STARTUP_PARITY, current_head, live_remote_head=live_head)
def _current_runtime_mode_report(refresh: bool = False) -> dict:
@@ -16626,15 +16903,34 @@ def gitea_get_runtime_context(
"restart_required": parity["restart_required"],
"startup_head": parity["startup_head"],
"current_head": parity["current_head"],
# #610 distinguished mutation-safety signals:
"daemon_start_head": parity["daemon_start_head"],
"local_head": parity["local_head"],
"live_remote_head": parity["live_remote_head"],
"live_known": parity["live_known"],
"live_stale": parity["live_stale"],
"mutation_safe": parity["mutation_safe"],
"summary": master_parity_gate.format_parity(parity),
"mutation_gate_enforced": not master_parity_gate.gate_disabled(),
# #610: the capability resolver is authoritative for mutation safety;
# local parity alone must never authorize a mutation.
"resolver_authoritative_for_mutation_safety": True,
}
if parity["stale"] and not master_parity_gate.gate_disabled():
safe_next_action = (
"Server code is stale relative to master; restart the Gitea MCP "
"server to load current capability gates before mutating. "
f"({master_parity_gate.format_parity(parity)})"
)
if parity["restart_required"] and not master_parity_gate.gate_disabled():
if parity["live_stale"]:
safe_next_action = (
"Daemon is stale relative to LIVE remote master "
f"(started {parity['startup_head'][:12] if parity['startup_head'] else 'unknown'}, "
f"live master {parity['live_remote_head'][:12] if parity['live_remote_head'] else 'unknown'}); "
"restart/reconnect the Gitea MCP server before mutating. The "
"capability resolver is authoritative for mutation safety."
)
else:
safe_next_action = (
"Server code is stale relative to master; restart the Gitea MCP "
"server to load current capability gates before mutating. "
f"({master_parity_gate.format_parity(parity)})"
)
result["safe_next_action"] = safe_next_action
if reveal and h:
@@ -16683,6 +16979,13 @@ def gitea_assess_master_parity(
"determinable": parity["determinable"],
"startup_head": parity["startup_head"],
"current_head": parity["current_head"],
# #610 distinguished mutation-safety signals:
"daemon_start_head": parity["daemon_start_head"],
"local_head": parity["local_head"],
"live_remote_head": parity["live_remote_head"],
"live_known": parity["live_known"],
"live_stale": parity["live_stale"],
"mutation_safe": parity["mutation_safe"],
"mutation_gate_enforced": enforced,
"summary": master_parity_gate.format_parity(parity),
"reasons": parity["reasons"],
@@ -16699,7 +17002,7 @@ def gitea_assess_master_parity(
source=canonical_source,
),
}
if parity["stale"] and enforced:
if parity["restart_required"] and enforced:
out["report"] = master_parity_gate.parity_report(parity)
return out