Files
cleveragents-core/tools/_implementer_compliance_apply.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

604 lines
22 KiB
Python

"""Deterministic applier for PR-compliance-checklist gaps.
What this is
------------
The implementer's compliance scan (:mod:`_implementer_compliance`)
checks four items mechanically:
- ``worktree_clean`` — sanity check, not fixable
- ``changelog_unreleased_nonempty`` — needs an entry under [Unreleased]
- ``contributors_has_author`` — needs the author email in CONTRIBUTORS.md
- ``commit_has_issues_closed`` — needs ``ISSUES CLOSED: #N`` in HEAD commit
Three of those (``contributors_has_author``, ``commit_has_issues_closed``,
``changelog_unreleased_nonempty``) can be applied without LLM reasoning
when the dispatcher has the source data:
- CONTRIBUTORS line: ``- {GIT_USER_NAME} <{GIT_USER_EMAIL}>``
- ISSUES CLOSED footer: derived from the prefetched ``linked_issues``
- CHANGELOG bullet: stubbed from the PR title (LLM-quality would be
marginally better here, but a PR-title stub is fine for most cases)
This module exposes :func:`apply_compliance_fixes` which takes a
worktree path + the prefetched data + the gap dict and applies the
fixes. It does NOT push — pushing is the caller's responsibility
(``dispatch_implementer._auto_fix_and_push``) because push failure
recovery needs cycle-level context.
The fixes are deterministic Python (no LLM). Every operation is
idempotent: calling :func:`apply_compliance_fixes` twice on a
worktree that's already had a fix applied is a no-op for that gap.
Why this exists
---------------
Live test 2026-05-13 (PR #28 cycle 1) showed the worker uses ~12-14
min of LLM time + 4096 server time on what's effectively five bash
commands. The same operations run deterministically in <5 seconds
with no LLM cost. ``auto-agents.md`` explicitly endorses this
pattern: "deterministic Python owns orchestration; the LLM only
handles the actual creative work."
The classes covered here are *not* creative work. Writing a real
code fix for a failing test IS creative work and stays on the LLM.
Failure handling
----------------
Each fix function returns ``(applied: bool, error: str | None)``.
:func:`apply_compliance_fixes` collects the results and propagates
them to the caller. On any error, the caller (the dispatcher's
auto-fix path) should fall through to spawning the LLM worker
rather than half-fixing the PR.
Audit trail
-----------
Each applied fix produces a structured entry in the returned
``ApplyReport`` (gap name, command sequence, before/after marker).
The dispatcher writes this into the cycle archive so an operator
inspecting why a commit appeared can trace it back to the
deterministic applier rather than wondering which LLM session
produced it.
"""
from __future__ import annotations
import logging
import re
import subprocess
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
_logger = logging.getLogger("implementer_compliance_apply")
@dataclass
class ApplyResult:
"""Per-gap outcome of an apply attempt.
``applied``: True when the fix wrote something (or was already
satisfied — idempotent). False when an error prevented the
apply.
``error``: Human-readable error string. None on success.
``operations``: Ordered list of (command, output) pairs the
apply ran. Used by the cycle archive for audit-trail.
"""
gap_name: str
applied: bool
error: str | None = None
operations: list[tuple[str, str]] = field(default_factory=list)
@dataclass
class ApplyReport:
"""Aggregate outcome of all attempted gap fixes.
``per_gap``: ApplyResult for each gap the caller asked to fix.
``all_applied``: True iff every requested gap fix succeeded.
``any_committed``: True if the apply created a new git commit
(the caller needs to know whether to push). A pure CONTRIBUTORS
update needs a commit. An ISSUES CLOSED amend re-uses HEAD's
commit (no new SHA). The combined fix is a single new commit
on top of HEAD.
"""
per_gap: list[ApplyResult] = field(default_factory=list)
all_applied: bool = True
any_committed: bool = False
final_head_sha: str = ""
def apply_compliance_fixes(
worktree: Path,
gaps: dict[str, bool],
*,
git_user_name: str,
git_user_email: str,
pr_title: str,
pr_number: int,
linked_issue_numbers: list[int],
) -> ApplyReport:
"""Apply deterministic fixes for the False entries in ``gaps``.
``gaps`` follows the contract from
:func:`_implementer_compliance.check_compliance_gaps` — keys are
check names, values are bool (True = passing, False = needs fix).
``linked_issue_numbers`` is the list of issue numbers the PR
closes (sourced from the prefetched ``linked_issues``). When
multiple issues are linked we include all of them in the
``ISSUES CLOSED`` footer. When the list is empty AND the
``commit_has_issues_closed`` gap is open, that fix is SKIPPED
(no data to apply) and recorded as an error in the report so
the caller falls through to the LLM worker.
The fix order is:
1. Apply CONTRIBUTORS append (file edit, staged)
2. Apply CHANGELOG stub (file edit, staged)
3. Stage and commit those two edits together
4. Amend the HEAD commit message to add ISSUES CLOSED footer
(works whether step 3 created a new commit or not; amending
the worker's previous commit is safe because we're in a
per-PR worktree)
Ordering matters: amend MUST be last because it rewrites HEAD.
"""
report = ApplyReport()
# Stage edits first; commit happens once for the file edits;
# amend at the end.
staged_changes = False
if not gaps.get("contributors_has_author", True):
result = _apply_contributors(worktree, git_user_name, git_user_email)
report.per_gap.append(result)
if result.applied and not result.error:
staged_changes = True
else:
report.all_applied = False
if not gaps.get("changelog_unreleased_nonempty", True):
result = _apply_changelog(worktree, pr_title, pr_number)
report.per_gap.append(result)
if result.applied and not result.error:
staged_changes = True
else:
report.all_applied = False
# Commit staged file changes (if any) before potentially amending
# the message in the next step. Doing it in two phases keeps the
# commit-message amend cleanly separable from the content add.
if staged_changes:
commit_result = _commit_staged(
worktree,
git_user_name,
git_user_email,
pr_title,
pr_number,
linked_issue_numbers,
)
report.per_gap.append(commit_result)
if commit_result.applied and not commit_result.error:
report.any_committed = True
else:
report.all_applied = False
# ISSUES CLOSED amend: applies whether or not we just committed.
# If we did commit above, the message already includes the
# footer (from `_commit_staged`); the amend is then a no-op
# idempotent re-check. If no file changes were needed, this
# amends the existing HEAD commit.
if not gaps.get("commit_has_issues_closed", True):
if not linked_issue_numbers:
report.per_gap.append(
ApplyResult(
gap_name="commit_has_issues_closed",
applied=False,
error=(
"no linked_issue_numbers — cannot derive "
"ISSUES CLOSED footer deterministically; "
"falling through to LLM worker"
),
)
)
report.all_applied = False
elif not staged_changes:
# Amend the EXISTING HEAD commit message rather than
# creating a new commit. This matches what the worker
# does today via the git-commit-util sub-agent.
amend_result = _amend_commit_footer(
worktree,
git_user_name,
git_user_email,
linked_issue_numbers,
)
report.per_gap.append(amend_result)
if amend_result.applied and not amend_result.error:
report.any_committed = True
else:
report.all_applied = False
# else: the new commit from _commit_staged already carries
# the ISSUES CLOSED footer; nothing more to do.
report.final_head_sha = _rev_parse_head(worktree)
return report
# ─── Per-gap appliers ──────────────────────────────────────────────
def _apply_contributors(
worktree: Path,
git_user_name: str,
git_user_email: str,
) -> ApplyResult:
"""Append ``- {name} <{email}>`` to CONTRIBUTORS.md if absent.
Creates the file with a minimal header when it doesn't exist.
Idempotent — if the line is already present the function
returns ``applied=True`` without writing.
"""
result = ApplyResult(gap_name="contributors_has_author", applied=False)
contributors = worktree / "CONTRIBUTORS.md"
line = f"- {git_user_name} <{git_user_email}>"
try:
if contributors.exists():
text = contributors.read_text(encoding="utf-8")
if git_user_email.lower() in text.lower():
# already present — idempotent success
result.applied = True
result.operations.append(
("read CONTRIBUTORS.md", "email already present")
)
return result
if not text.endswith("\n"):
text += "\n"
text += line + "\n"
else:
text = (
"# Contributors\n\n"
"Thanks to everyone who has contributed to this project:\n\n"
f"{line}\n"
)
contributors.write_text(text, encoding="utf-8")
result.applied = True
result.operations.append((f"write CONTRIBUTORS.md (+1 line)", line))
except OSError as exc:
result.error = f"CONTRIBUTORS.md write failed: {exc}"
return result
def _apply_changelog(
worktree: Path,
pr_title: str,
pr_number: int,
) -> ApplyResult:
"""Insert a stub bullet under [Unreleased] in CHANGELOG.md.
The stub uses the PR title verbatim. If CHANGELOG.md exists but
has no [Unreleased] section, one is inserted at the top below
the first ``# Changelog`` header. If CHANGELOG.md does not
exist, a minimal one is created.
The bullet appears as ``- {pr_title} (#{pr_number})`` to give a
cross-reference back to the PR. LLM-quality CHANGELOG entries
might say more, but the PR-title stub is the dominant pattern
in this codebase's history and is correct for most cases.
Idempotent: if a bullet referencing ``#{pr_number}`` already
exists under [Unreleased], no write happens.
"""
result = ApplyResult(gap_name="changelog_unreleased_nonempty", applied=False)
changelog = worktree / "CHANGELOG.md"
bullet = f"- {pr_title.strip()} (#{pr_number})"
try:
if changelog.exists():
text = changelog.read_text(encoding="utf-8")
# Idempotent guard
if f"(#{pr_number})" in text:
result.applied = True
result.operations.append(
("read CHANGELOG.md", "bullet for this PR already present")
)
return result
else:
text = "# Changelog\n\n## [Unreleased]\n\n"
# Find the [Unreleased] header; insert bullet under it.
unreleased_re = re.compile(
r"^(##\s*\[Unreleased\][^\n]*\n)",
re.MULTILINE | re.IGNORECASE,
)
m = unreleased_re.search(text)
if m:
insert_at = m.end()
# Insert a blank line + bullet + blank line if the
# header is immediately followed by content. Otherwise
# just bullet + blank line.
tail = text[insert_at:]
if tail.startswith("\n"):
# there's a blank after the header; just inject
new_text = text[:insert_at] + bullet + "\n" + tail
else:
new_text = text[:insert_at] + "\n" + bullet + "\n" + tail
else:
# No [Unreleased] section — add one near the top.
header_re = re.compile(r"^(#\s+[^\n]+\n)", re.MULTILINE)
hm = header_re.search(text)
new_section = f"\n## [Unreleased]\n\n{bullet}\n"
if hm:
new_text = text[: hm.end()] + new_section + text[hm.end() :]
else:
# No top-level header either; prepend everything.
new_text = f"# Changelog\n{new_section}\n{text}"
changelog.write_text(new_text, encoding="utf-8")
result.applied = True
result.operations.append(("write CHANGELOG.md (+1 bullet)", bullet))
except OSError as exc:
result.error = f"CHANGELOG.md write failed: {exc}"
return result
# ─── Git operations ────────────────────────────────────────────────
def _commit_staged(
worktree: Path,
git_user_name: str,
git_user_email: str,
pr_title: str,
pr_number: int,
linked_issue_numbers: list[int],
) -> ApplyResult:
"""Stage all changes in the worktree and commit them with a
compliance-fix-shaped commit message.
The commit message embeds the ``ISSUES CLOSED:`` footer pre-
populated from ``linked_issue_numbers`` so the subsequent amend
step is a no-op when the new commit is the HEAD. If
``linked_issue_numbers`` is empty, the message has no footer —
the amend step will then detect the missing footer and either
add it (if data is available) or leave the gap.
"""
result = ApplyResult(gap_name="commit_staged_compliance_fixes", applied=False)
msg_lines = [
f"chore(compliance): apply deterministic compliance fixes for #{pr_number}",
"",
f"Auto-applied by the implementer dispatcher's compliance scanner.",
f"Source PR: #{pr_number}{pr_title.strip()}",
]
if linked_issue_numbers:
joined = " ".join(f"#{n}" for n in linked_issue_numbers)
msg_lines.append("")
msg_lines.append(f"ISSUES CLOSED: {joined}")
msg = "\n".join(msg_lines) + "\n"
try:
env = _env_for_commit(git_user_name, git_user_email)
_run_git(worktree, ["add", "-A"], env, result, "stage all")
_run_git(worktree, ["commit", "-m", msg], env, result, "commit")
result.applied = True
except _GitError as exc:
result.error = f"commit_staged failed: {exc}"
return result
def _amend_commit_footer(
worktree: Path,
git_user_name: str,
git_user_email: str,
linked_issue_numbers: list[int],
) -> ApplyResult:
"""Amend HEAD to add an ``ISSUES CLOSED: #N #M`` footer.
Reads the current message, appends the footer if not present,
rewrites via ``git commit --amend``. Idempotent: if the footer
is already there with the same issue numbers, no rewrite.
"""
result = ApplyResult(gap_name="commit_has_issues_closed", applied=False)
if not linked_issue_numbers:
result.error = "no linked_issue_numbers — cannot amend"
return result
try:
env = _env_for_commit(git_user_name, git_user_email)
# Read current message
rc, out = _run_git_capture(
worktree,
["log", "-1", "--pretty=%B", "HEAD"],
env,
)
if rc != 0:
result.error = f"git log -1 returned {rc}"
return result
current_msg = out.rstrip("\n")
joined = " ".join(f"#{n}" for n in linked_issue_numbers)
footer = f"ISSUES CLOSED: {joined}"
if footer in current_msg:
# Already there — idempotent success
result.applied = True
result.operations.append(("read HEAD message", "footer already present"))
return result
new_msg = current_msg.rstrip() + "\n\n" + footer + "\n"
_run_git(
worktree,
["commit", "--amend", "-m", new_msg],
env,
result,
"amend HEAD with footer",
)
result.applied = True
except _GitError as exc:
result.error = f"amend failed: {exc}"
return result
def _rev_parse_head(worktree: Path) -> str:
"""Return HEAD's SHA in the worktree, or empty string on error."""
try:
proc = subprocess.run(
["git", "-C", str(worktree), "rev-parse", "HEAD"],
capture_output=True,
text=True,
timeout=10,
check=False,
)
if proc.returncode == 0:
return proc.stdout.strip()
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
return ""
# ─── Internal helpers ──────────────────────────────────────────────
class _GitError(Exception):
"""Raised by ``_run_git`` when a subprocess fails."""
def _env_for_commit(name: str, email: str) -> dict[str, str]:
"""Build an env override that pins the commit identity AND
disables GPG signing (otherwise dev machines with global signing
enabled would prompt or fail).
"""
import os
env = {**os.environ}
env["GIT_AUTHOR_NAME"] = name
env["GIT_AUTHOR_EMAIL"] = email
env["GIT_COMMITTER_NAME"] = name
env["GIT_COMMITTER_EMAIL"] = email
return env
def _run_git(
worktree: Path,
args: list[str],
env: dict[str, str],
result: ApplyResult,
label: str,
) -> None:
"""Run a git subcommand; raise ``_GitError`` on non-zero exit.
Records the operation in ``result.operations`` for audit trail.
Also unconditionally adds ``-c commit.gpgsign=false`` to defend
against developer-machine global signing configs (the live test
fixture pattern).
"""
cmd = ["git", "-c", "commit.gpgsign=false", "-C", str(worktree), *args]
try:
proc = subprocess.run(
cmd,
env=env,
capture_output=True,
text=True,
timeout=30,
check=False,
)
except (subprocess.TimeoutExpired, FileNotFoundError) as exc:
result.operations.append((" ".join(cmd[:8]), f"<failed: {exc}>"))
raise _GitError(f"{label}: {exc}") from exc
out = ((proc.stdout or "") + (proc.stderr or "")).strip()
result.operations.append((" ".join(cmd[:8]) + " ...", out[:200]))
if proc.returncode != 0:
raise _GitError(f"{label}: rc={proc.returncode}: {out[:300]}")
def _run_git_capture(
worktree: Path,
args: list[str],
env: dict[str, str],
) -> tuple[int, str]:
"""Run a git subcommand and return ``(rc, stdout)``. Stderr
discarded — for these read-only commands (``log``, ``rev-parse``)
we only care about stdout."""
cmd = ["git", "-C", str(worktree), *args]
try:
proc = subprocess.run(
cmd,
env=env,
capture_output=True,
text=True,
timeout=10,
check=False,
)
except (subprocess.TimeoutExpired, FileNotFoundError):
return -1, ""
return proc.returncode, (proc.stdout or "")
# ─── Push helper ───────────────────────────────────────────────────
def push_branch(
worktree: Path,
branch: str,
*,
git_env: dict[str, str],
) -> ApplyResult:
"""Push the worktree's HEAD to ``origin/<branch>``.
``git_env`` MUST be a credential-bearing git environment — the
one ``_pr_clone_creds._git_env(cfg)`` builds, carrying the
``GIT_ASKPASS`` shim that supplies the Forgejo PAT. The
dispatcher's pre-cloned worktree's ``origin`` is a bare HTTPS URL
with NO embedded credentials (by design — see
``_pr_clone._clone_url``), so a push run under a plain env fails
with ``fatal: could not read Username`` (rc=128) and the auto-fix
silently falls through to the LLM worker. Handing in the askpass
env — the same env the pre-clone/fetch already run under — is the
only thing that lets the deterministic auto-fix push authenticate.
The author/committer identity is irrelevant here: ``git push``
creates no commits, so this env need not (and does not) carry the
``GIT_AUTHOR_*`` / ``GIT_COMMITTER_*`` vars ``_env_for_commit``
sets for the commit/amend steps.
Uses ``--force-with-lease`` so a remote that moved while we were
applying gets a "stale info" rejection rather than a silent
overwrite. Disables mirror semantics via ``-c
remote.origin.mirror=false`` (the worker's per-command
workaround, which is robust across worktree configs even after
A1's clone-time fix lands).
"""
result = ApplyResult(gap_name="push_to_origin", applied=False)
cmd = [
"git",
"-c",
"remote.origin.mirror=false",
"-C",
str(worktree),
"push",
"--force-with-lease",
"origin",
f"HEAD:refs/heads/{branch}",
]
try:
proc = subprocess.run(
cmd,
env=git_env,
capture_output=True,
text=True,
timeout=60,
check=False,
)
out = ((proc.stdout or "") + (proc.stderr or "")).strip()
result.operations.append(("git push --force-with-lease ...", out[:300]))
if proc.returncode == 0:
result.applied = True
else:
result.error = f"push rc={proc.returncode}: {out[:300]}"
except (subprocess.TimeoutExpired, FileNotFoundError) as exc:
result.error = f"push subprocess failed: {exc}"
return result
__all__ = (
"ApplyResult",
"ApplyReport",
"apply_compliance_fixes",
"push_branch",
)