Files
cleveragents-core/tools/controller/worker/git_push.py
T
drew a103a31bbf feat(controller): worker-owned gated push for the implementer
The implementer agent no longer pushes to git — the controller worker
now owns the push: it gates the agent's commits on lint+typecheck and
pushes via a single leased primitive. Closes two production defects:

- Clobber: the pre-fix MCP --force-with-lease leased against a
  freshly-fetched tip, so the lease always passed — an in-flight
  implementer destroyed a commit pushed to the PR branch during its
  run (lost a hand-pushed skip_coverage fix on PR #46).
- Gate-skip: the agent verified only the CI-flagged gate, so a fix for
  one gate shipped fresh violations in another (lint flapped
  pass->fail across CI runs 198->199).

Step 1 — worker_push primitive:
- New git_push.py: one leased push, pinned to the SHA the worker
  started from; classifies pushed / stale_input / diverged /
  infra_error; bounded infra-retry.
- mcp_git_server.push gains expected_sha for a correctly-pinned lease.
- finalize_conflict_resolution migrated onto worker_push.

Step 2 — deterministic gate:
- New gate.py: per-slot nox env-dirs (no venv races), manifest-hash
  staleness keying, lazy warm-up.
- local_ci_gate.sh gains --envdir.

Step 3 — worker-owned gated push:
- New implementer_finalize.py: divergence pre-check -> lint+typecheck
  gate -> ruff auto-fix -> leased push. finalize's outcome is
  authoritative over the agent's emitted outcome.
- agent_runner integrates finalize; salvage no longer pushes.
- outcomes/tick: gate-failed + push-time stale-input caps,
  epoch-scoped; WorkerError carries an output_payload so the gate
  report reaches the next attempt's prompt; prefetch surfaces
  gate-failed attempts.
- The 5 task-implementor prompts drop the agent push step.

Reviewed across 4 adversarial rounds; full controller+MCP suite green
(1379 passed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:38:52 -04:00

229 lines
8.0 KiB
Python

"""Single deterministic git-push primitive for the controller worker.
Every worker-side push — implementer finalize, conflict-resolution
finalize, salvage — goes through :func:`worker_push` so the lease /
force / stale-detection semantics live in exactly one place.
Design (controller spec v3):
- The push is **leased against the SHA the worker started from**
(``expected_sha``), never a freshly-fetched remote tip. A concurrent
push that moved the branch is therefore *detected*, not silently
clobbered (the pre-fix MCP push leased against a just-fetched tip,
so the lease always passed — it destroyed an out-of-band commit).
- ``allow_rewrite=False`` (implementer): the worktree branch is a
fast-forward of ``expected_sha``; a plain push is used and *cannot*
clobber. A non-fast-forward rejection means the remote moved
→ ``stale_input``. A worktree HEAD that is NOT a descendant of
``expected_sha`` (the agent rewrote history) → ``diverged``.
- ``allow_rewrite=True`` (conflict-resolver rebase track): history was
rewritten on purpose; ``--force-with-lease`` pinned to
``expected_sha`` performs the rewrite while still refusing to
clobber a concurrently-moved branch.
- Transient infra failures (network / auth) are retried a bounded
number of times; ref-rejection failures are never retried — they
surface as ``stale_input`` so the caller re-prefetches.
"""
from __future__ import annotations
import subprocess
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Literal
PushStatus = Literal["pushed", "stale_input", "diverged", "infra_error"]
# stderr fragments that mark a genuine *ref rejection* — the remote
# moved under us — as opposed to a transport/auth/policy failure.
# Matched case-insensitively. These are the PRECISE markers git prints
# for a non-fast-forward / stale-lease rejection; the generic summary
# line ("failed to push some refs") is deliberately NOT included — it
# accompanies every push failure (hook declines, permission denied,
# quota), and matching it would misclassify those as stale_input.
_REJECTION_MARKERS = (
"non-fast-forward",
"fetch first",
"stale info",
"[rejected]",
"cannot lock ref",
)
@dataclass(frozen=True)
class PushResult:
"""Outcome of a :func:`worker_push`.
- ``pushed`` — the push landed; ``remote_sha`` is the verified tip.
- ``stale_input`` — the remote moved off ``expected_sha`` during the
attempt (detected before OR at push time). The caller must
re-prefetch; nothing was overwritten.
- ``diverged`` — the worktree HEAD is not a fast-forward of
``expected_sha`` (a non-rewrite caller's agent rewrote history).
- ``infra_error`` — a transport / auth / verification failure that
bounded retries did not clear.
"""
status: PushStatus
remote_sha: str = ""
detail: str = ""
def _run(
worktree: Path,
*args: str,
timeout: float = 180.0,
env: dict | None = None,
) -> tuple[int, str, str]:
"""Run a git command in ``worktree``; return ``(rc, stdout, stderr)``.
Never raises — callers branch on the return code."""
try:
r = subprocess.run(
["git", "-C", str(worktree), *args],
capture_output=True,
text=True,
timeout=timeout,
env=env,
)
except subprocess.TimeoutExpired:
return 124, "", "git command timed out"
return r.returncode, r.stdout, r.stderr
def _is_rejection(stderr: str) -> bool:
low = (stderr or "").lower()
return any(m in low for m in _REJECTION_MARKERS)
def worker_push(
worktree: Path,
*,
remote: str = "origin",
head_ref: str,
expected_sha: str,
allow_rewrite: bool,
env: dict | None = None,
timeout: float = 180.0,
infra_retries: int = 2,
_sleep=time.sleep,
) -> PushResult:
"""Push the worktree HEAD to ``<remote>/<head_ref>``, leased against
``expected_sha`` (the SHA the worker validated + reset to at setup).
See the module docstring for the ``allow_rewrite`` contract. The
``_sleep`` parameter is an injection point for tests.
"""
worktree = Path(worktree)
tracking = f"{remote}/{head_ref}"
refspec_fetch = (
"fetch",
remote,
f"+{head_ref}:refs/remotes/{remote}/{head_ref}",
)
# 1. Observe the current remote tip. The explicit-refspec form only
# touches the remote-tracking ref, so it works even when
# <head_ref> is the worktree's checked-out branch.
rc, _, ferr = _run(
worktree, *refspec_fetch, timeout=min(timeout, 60.0), env=env
)
if rc != 0:
return PushResult(
"infra_error",
detail=f"pre-push fetch failed: {ferr.strip()[:300]}",
)
rc, out, _ = _run(worktree, "rev-parse", tracking, timeout=30.0, env=env)
if rc != 0:
return PushResult("infra_error", detail=f"could not resolve {tracking}")
remote_tip = out.strip()
# 2. Lease — pinned to the SHA the worker started from. A mismatch
# means a concurrent push moved the branch; bail without pushing.
if remote_tip != expected_sha:
return PushResult(
"stale_input",
detail=(
f"{tracking} moved {expected_sha[:12]} -> "
f"{remote_tip[:12]} during the attempt"
),
)
rc, out, _ = _run(worktree, "rev-parse", "HEAD", timeout=30.0, env=env)
if rc != 0:
return PushResult("infra_error", detail="could not resolve worktree HEAD")
local_head = out.strip()
# 3. Fast-forward contract for non-rewrite callers: the worktree
# HEAD must descend from expected_sha. If not, the agent rewrote
# history — that is the agent's error, not a stale remote.
if not allow_rewrite:
anc_rc, _, _ = _run(
worktree,
"merge-base",
"--is-ancestor",
expected_sha,
"HEAD",
timeout=30.0,
env=env,
)
if anc_rc == 1:
return PushResult(
"diverged",
detail=(
f"worktree HEAD {local_head[:12]} is not a fast-forward "
f"of base {expected_sha[:12]}"
),
)
if anc_rc not in (0, 1):
return PushResult(
"infra_error", detail="merge-base --is-ancestor errored"
)
push_cmd: list[str] = ["push", remote, f"HEAD:refs/heads/{head_ref}"]
if allow_rewrite:
push_cmd.append(
f"--force-with-lease=refs/heads/{head_ref}:{expected_sha}"
)
# 4. Push. Retry only transient infra failures; a ref rejection is
# a real concurrent push and surfaces immediately as stale_input.
last_detail = ""
pushed = False
for attempt in range(infra_retries + 1):
rc, _, perr = _run(worktree, *push_cmd, timeout=timeout, env=env)
if rc == 0:
pushed = True
break
if _is_rejection(perr):
return PushResult(
"stale_input",
detail=f"push rejected (remote moved): {perr.strip()[:300]}",
)
last_detail = f"push failed: {perr.strip()[:300]}"
if attempt < infra_retries:
_sleep(2.0 * (attempt + 1))
if not pushed:
return PushResult("infra_error", detail=last_detail)
# 5. Verify the push actually landed on the remote.
_run(worktree, *refspec_fetch, timeout=min(timeout, 60.0), env=env)
rc, out, _ = _run(worktree, "rev-parse", tracking, timeout=30.0, env=env)
landed = out.strip() if rc == 0 else ""
if landed != local_head:
return PushResult(
"infra_error",
detail=(
f"push reported success but {tracking}={landed[:12]} != "
f"HEAD={local_head[:12]}"
),
)
return PushResult(
"pushed",
remote_sha=local_head,
detail=f"pushed {local_head[:12]} to {tracking}",
)
__all__ = ["PushResult", "PushStatus", "worker_push"]