Files
cleveragents-core/tools/_implementer_escalation_helpers.py
T
drew 0bc734c020 style: ruff format the controller-state-machine branch (288 files)
Applies `ruff format` to the accumulated formatting debt on this branch.
Formatting-only — no behavioral changes. Required for CI/lint's format
gate (`nox -s format -- --check`), which the branch was failing on 288
tracked files that drifted from ruff's canonical style.

In-progress WIP files are intentionally excluded so this commit stays a
clean formatting-only diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 00:09:17 -04:00

187 lines
6.4 KiB
Python

"""Pure helpers extracted from :mod:`dispatch_implementer`'s
in-cycle tier escalation loop.
The orchestrator (``_post_session_action_with_escalation``) stays in
the dispatcher because it has tight coupling to telemetry, post-
session action wiring, and the WORK_GROUPS registry. The functions
here have no such coupling — they're side-effecting OS / git /
HTTP calls or pure data transforms — so they extract cleanly and
each gains its own test surface in :mod:`tests.auto_agents.test_dispatch_implementer`.
The dispatcher imports the public names below and delegates verbatim.
"""
from __future__ import annotations
import logging
import os
import subprocess
import sys
from pathlib import Path
from typing import Any
_TOOLS_DIR = str(Path(__file__).resolve().parent)
if _TOOLS_DIR not in sys.path:
sys.path.insert(0, _TOOLS_DIR)
from _loader import ( # noqa: E402 type: ignore[import-not-found]
load_sibling as _load_sibling,
)
_logger = logging.getLogger("implementer_escalation_helpers")
def fetch_pr_state(cfg: Any, pr_number: int, *, claim_runtime: Any) -> str:
"""GET the PR state. Returns ``"open"`` / ``"closed"`` / ``"merged"``.
Conservative fallback to ``"open"`` on any failure so the escalation
loop doesn't spuriously end a cycle on a transient blip.
``claim_runtime`` is the HTTP shim module the caller is using; the
dispatcher passes its own reference so tests that fresh-reload
``_claim_runtime`` see their fresh instance, not whatever was
cached when this helper module first imported."""
if cfg.dry_run or pr_number <= 0:
return "open"
try:
response = claim_runtime.get(
f"/repos/{cfg.owner}/{cfg.repo}/pulls/{int(pr_number)}", cfg
)
except (OSError, ValueError) as exc:
_logger.warning(
"PR state fetch failed for #%s (treating as 'open'): %s",
pr_number,
exc,
)
return "open"
if int(response.get("status") or 0) != 200:
return "open"
body = response.get("body") or {}
if not isinstance(body, dict):
return "open"
state = body.get("state")
if isinstance(state, str) and state:
# Forgejo exposes "merged" via the `merged` boolean on
# state=closed PRs; honour that distinction.
if state == "closed" and body.get("merged") is True:
return "merged"
return state
return "open"
def reset_worktree_to_pinned_sha(handle: Any) -> bool:
"""Reset the pre-cloned worktree to the SHA captured at prefetch
time. Returns ``True`` on success, ``False`` on any failure
(logged WARNING; caller continues — the next session's discover
falls through to ``git-isolator-util``).
Defends against worker-side state corruption: missing worktree
dir (the worker may have ``rm -rf``'d it), stale ``.git/*.lock``
files from SIGKILL'd worker git ops.
"""
if handle is None:
return False
path = getattr(handle, "path", None)
pinned_sha = getattr(handle, "head_sha", None)
if not path or not pinned_sha:
return False
sha_short = str(pinned_sha)[:12]
if not Path(path).is_dir():
_logger.warning(
"worktree disappeared before reset for SHA %s at %s"
"the prior worker session likely deleted it. Escalation "
"will continue; the next session's "
"``implementer-workspace.py discover`` will fall through "
"to ``git-isolator-util`` for a fresh clone.",
sha_short,
path,
)
return False
try:
git_dir = Path(path) / ".git"
if git_dir.is_dir():
for lock in git_dir.glob("*.lock"):
if lock.is_file():
lock.unlink()
_logger.info(
"removed stale .git/%s at %s before reset",
lock.name,
path,
)
except OSError:
pass
try:
subprocess.run(
["git", "-C", str(path), "reset", "--hard", str(pinned_sha)],
check=True,
capture_output=True,
timeout=30,
text=True,
)
subprocess.run(
["git", "-C", str(path), "clean", "-xfdq"],
check=True,
capture_output=True,
timeout=30,
text=True,
)
return True
except subprocess.CalledProcessError as exc:
# CalledProcessError.__str__ shows only exit code; include
# captured stderr so the operator can diagnose (vanished SHA
# vs. lock contention vs. permissions).
_logger.warning(
"worktree reset to pinned SHA %s failed at %s: "
"exit=%s stderr=%r; escalation continues",
sha_short,
path,
exc.returncode,
(exc.stderr or "").strip()[:400],
)
return False
except subprocess.SubprocessError as exc:
_logger.warning(
"worktree reset to pinned SHA %s raised %s at %s; escalation continues",
sha_short,
type(exc).__name__,
path,
)
return False
def per_tier_worker_timeout(cfg: Any, tier: int) -> int:
"""Resolve the worker timeout for ``tier``. Per-tier override via
``IMPLEMENTER_DISPATCHER_WORKER_TIMEOUT_TIER_{N}_SECONDS``; falls
back to ``cfg.worker_timeout_seconds``.
Higher tiers historically need more wallclock (tier-2 escalation
can legitimately run for hours); operators tune this per-slot,
not per-model, so it stays correct across model swaps."""
env_name = f"IMPLEMENTER_DISPATCHER_WORKER_TIMEOUT_TIER_{int(tier)}_SECONDS"
raw = os.environ.get(env_name)
if raw:
try:
return max(60, int(raw))
except ValueError:
_logger.warning(
"%s=%r is not an int; falling back to global timeout",
env_name,
raw,
)
return int(cfg.worker_timeout_seconds)
def terminal_state_from_session(session: Any) -> str:
"""Mirror dispatch_one's terminal_state derivation: ``completed``
for the happy path, raw ``status`` otherwise (``timeout`` /
``transport-error``)."""
return "completed" if session.status == "completed" else session.status
__all__ = (
"fetch_pr_state",
"per_tier_worker_timeout",
"reset_worktree_to_pinned_sha",
"terminal_state_from_session",
)