Files
cleveragents-core/tools/controller/worker/implementer_finalize.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

341 lines
12 KiB
Python

"""Deterministic post-agent finalize for implementer attempts.
The implementer agent edits + commits in the worktree and finalizes —
it no longer pushes. After the agent session ends the worker runs
:func:`finalize_implementer_attempt`, which:
1. checks the worktree HEAD is a fast-forward of the PR base (a
history-rewriting agent is a contract violation, not a stale remote);
2. runs the deterministic lint+typecheck gate, with a ruff auto-fix
pass for the trivially-fixable lint subset;
3. pushes via the leased :func:`worker_push` primitive.
The :class:`FinalizeResult` it returns — not the agent's emitted
outcome — is authoritative. A crash between push and the runner's DB
write self-heals: a re-run attempt's :func:`worker_push` sees the
remote already moved off the input head_sha and returns ``stale_input``
→ the master re-prefetches and rebuilds on the already-pushed work.
"""
from __future__ import annotations
import logging
import subprocess
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from .gate import acquire_slot, lint_ruff, release_slot, run_gates
from .git_push import worker_push
logger = logging.getLogger(__name__)
_GIT_IDENTITY = (
"-c",
"user.email=cleveragents-auto@cleveragents.local",
"-c",
"user.name=cleveragents-auto",
)
# Worker-authored finalize outcomes the runner must surface as a
# WorkerError (status='failed' → the master re-enqueues). A 'complete'
# attempt with no state-machine event is NOT re-dispatched by the
# scheduler — so gate-failed MUST travel this path, not the completed
# path, or a gate-failed implementer would stall the workflow.
WORKER_ERROR_OUTCOMES = frozenset(
{"stale-input", "worker-internal-error", "gate-failed"}
)
@dataclass(frozen=True)
class FinalizeResult:
"""Authoritative outcome of an implementer attempt.
``outcome`` is one of: ``resolved`` (gated + pushed), ``gate-failed``
(lint/typecheck failed on the agent's commits), ``blocked`` (the
agent rewrote history), ``stale-input`` (the branch moved during the
attempt), ``worker-internal-error`` (infra), or a passthrough of the
agent's own outcome when the agent made no commits.
"""
outcome: str
head_sha_after: str
output_payload: dict[str, Any]
detail: str = ""
def _git(worktree: Path, *args: str, timeout: float = 120.0) -> tuple[int, str, str]:
try:
r = subprocess.run(
["git", "-C", str(worktree), *args],
capture_output=True,
text=True,
timeout=timeout,
)
except subprocess.TimeoutExpired:
return 124, "", "git command timed out"
return r.returncode, r.stdout, r.stderr
def _git_ok(worktree: Path, *args: str, timeout: float = 120.0) -> str:
rc, out, err = _git(worktree, *args, timeout=timeout)
if rc != 0:
raise RuntimeError(f"git {' '.join(args)} failed: {err.strip()}")
return out
def _commits(worktree: Path, base: str, head: str) -> list[str]:
rc, out, _ = _git(worktree, "rev-list", "--reverse", f"{base}..{head}")
return out.split() if rc == 0 else []
def _changed_files(worktree: Path, base: str, head: str) -> list[str]:
rc, out, _ = _git(worktree, "diff", "--name-only", f"{base}..{head}")
return [ln.strip() for ln in out.splitlines() if ln.strip()] if rc == 0 else []
def _with(payload: dict[str, Any], **overrides: Any) -> dict[str, Any]:
return {**payload, **overrides}
def _attempt_ruff_autofix(
worktree: Path, files: list[str], slot_index: int
) -> bool:
"""Run ruff's deterministic auto-fix (safe lint fixes + format) on
the agent's touched ``.py`` files, using the SAME pinned ruff the
lint gate runs. Commits the result iff anything changed; returns
True iff a fix commit was made.
Note: ``ruff format`` may reformat lines the agent did not write
within a touched file — expected and harmless (it makes the file
CI-clean). The fix is scoped to the agent's touched files so it
never silently rewrites unrelated pre-existing lint debt.
"""
ruff = lint_ruff(slot_index)
if ruff is None:
return False
py_files = sorted({f for f in files if f.endswith(".py")})
if not py_files:
return False
# ``--fix`` applies only fixes ruff marks SAFE; ``--exit-zero`` so a
# remaining non-auto-fixable violation does not abort the format.
subprocess.run(
[str(ruff), "check", "--fix", "--exit-zero", *py_files],
cwd=str(worktree),
capture_output=True,
text=True,
timeout=180,
)
subprocess.run(
[str(ruff), "format", *py_files],
cwd=str(worktree),
capture_output=True,
text=True,
timeout=180,
)
rc, out, _ = _git(worktree, "status", "--porcelain")
if rc != 0 or not out.strip():
return False # ruff changed nothing
try:
_git_ok(worktree, "add", "--", *py_files)
_git_ok(
worktree,
*_GIT_IDENTITY,
"commit",
"-m",
"chore: worker ruff auto-fix (pre-push lint gate)",
)
except RuntimeError as exc:
logger.warning("ruff auto-fix commit failed: %s", exc)
return False
return True
def finalize_implementer_attempt(
*,
worktree: Path,
input_payload: dict[str, Any],
agent_output: dict[str, Any],
tier: int | None,
lost_lock_check: Callable[[], bool],
_run_gates: Callable[..., Any] = run_gates,
_worker_push: Callable[..., Any] = worker_push,
_autofix: Callable[..., bool] = _attempt_ruff_autofix,
) -> FinalizeResult:
"""Gate + push an implementer attempt; return the authoritative
outcome. See the module docstring for the contract."""
worktree = Path(worktree)
base = input_payload.get("head_sha")
head_ref = input_payload.get("head_ref")
if not (isinstance(base, str) and base and isinstance(head_ref, str) and head_ref):
return FinalizeResult(
"worker-internal-error",
base if isinstance(base, str) else "",
_with(agent_output, commit_shas=[]),
detail="input_payload missing head_sha / head_ref",
)
rc, out, err = _git(worktree, "rev-parse", "HEAD")
if rc != 0:
return FinalizeResult(
"worker-internal-error",
base,
_with(agent_output, commit_shas=[]),
detail=f"could not read worktree HEAD: {err.strip()}",
)
head = out.strip()
# finalize gates + pushes a CLAIMED-RESOLVED attempt. Honor the
# agent's own outcome — without gating or pushing — when there is
# nothing to ship: either no commits, OR the agent explicitly
# reported a non-resolved outcome (blocked / competence-failure /
# noop / ...). Pushing stray commits an agent disowned would ship
# partial work and silently override the agent's "I failed". The
# commit list is zeroed so head is not seen to advance.
agent_outcome = str(agent_output.get("outcome") or "noop")
if head == base or agent_outcome != "resolved":
return FinalizeResult(
agent_outcome,
base,
_with(agent_output, commit_shas=[]),
detail=(
"implementer produced no commits"
if head == base
else f"agent outcome {agent_outcome!r} is not 'resolved' "
"— not gating or pushing"
),
)
# Divergence: a non-rewrite implementer's HEAD must descend from the
# PR base. If not, the agent rewrote history — a contract violation,
# not a stale remote. Re-dispatch lands on a fresh reset-to-base.
anc_rc, _, _ = _git(worktree, "merge-base", "--is-ancestor", base, "HEAD")
if anc_rc == 1:
return FinalizeResult(
"blocked",
base,
_with(
agent_output,
outcome="blocked",
commit_shas=[],
blockers=[
"implementer rewrote history; worktree HEAD is not a "
"fast-forward of the PR base — re-dispatch on a clean base"
],
),
detail="worktree HEAD diverged from the PR base",
)
if anc_rc not in (0,):
return FinalizeResult(
"worker-internal-error",
base,
_with(agent_output, commit_shas=[]),
detail="merge-base --is-ancestor errored",
)
slot = acquire_slot()
try:
gate = _run_gates(worktree, slot_index=slot)
# Trivial lint regressions (import order, formatting) are
# deterministically auto-fixable — try that before spending a
# whole re-dispatch on it, then re-gate.
if gate.status == "fail" and gate.failing_gate == "lint":
files = _changed_files(worktree, base, head)
if _autofix(worktree, files, slot):
rc, out, _ = _git(worktree, "rev-parse", "HEAD")
head = out.strip() if rc == 0 else head
gate = _run_gates(worktree, slot_index=slot)
# Classify the (possibly re-run) gate result. ``error`` is an
# infra failure — NOT the agent's fault and NOT counted against
# the gate-retry budget — so it must be checked after the
# auto-fix re-gate too, never collapsed into ``gate-failed``.
if gate.status == "error":
return FinalizeResult(
"worker-internal-error",
base,
_with(agent_output, commit_shas=[]),
detail=f"gate could not run ({gate.failing_gate}): "
f"{gate.raw_tail[-300:]}",
)
if gate.status == "fail":
tail = (gate.raw_tail or "")[-1500:]
return FinalizeResult(
"gate-failed",
base,
_with(
agent_output,
outcome="gate-failed",
commit_shas=[], # nothing pushed → head must not advance
files_touched=_changed_files(worktree, base, head),
blockers=[
f"local {gate.failing_gate} gate failed before "
f"push:\n{tail}"
],
),
detail=f"{gate.failing_gate} gate failed",
)
# Gate green (possibly after the auto-fix). Push.
if lost_lock_check():
return FinalizeResult(
"worker-internal-error",
base,
_with(agent_output, commit_shas=[]),
detail="lock lost before push",
)
result = _worker_push(
worktree,
head_ref=head_ref,
expected_sha=base,
allow_rewrite=False,
)
finally:
release_slot(slot)
if result.status == "pushed":
return FinalizeResult(
"resolved",
result.remote_sha,
_with(
agent_output,
outcome="resolved",
commit_shas=_commits(worktree, base, result.remote_sha),
files_touched=_changed_files(worktree, base, result.remote_sha),
),
detail=result.detail,
)
if result.status == "stale_input":
return FinalizeResult(
"stale-input",
base,
_with(agent_output, commit_shas=[]),
detail=result.detail,
)
if result.status == "diverged":
return FinalizeResult(
"blocked",
base,
_with(
agent_output,
outcome="blocked",
commit_shas=[],
blockers=["worktree HEAD diverged from the PR base at push"],
),
detail=result.detail,
)
# infra_error
return FinalizeResult(
"worker-internal-error",
base,
_with(agent_output, commit_shas=[]),
detail=result.detail,
)
__all__ = [
"FinalizeResult",
"WORKER_ERROR_OUTCOMES",
"finalize_implementer_attempt",
]