Files
cleveragents-core/tools/verify_implementer_invariant.py
T
drew 2f1be34d12 feat(auto-agents): implementer parity — verify-invariant verifiers, implementer-helpers skill, watchdog gate, _opencode_worker audit
Closes the four open items in `docs/development/auto-agents-tier-2-3-plan.md`
§ "Revised remaining scope (2026-05-08)" plus three rounds of fresh-eyes
critique fold-in (rounds 3, 5, and post-round-5 polish).

Highlights:

- New continuous invariant verifiers on a shared `_verify_common.py`
  substrate: `verify_review_invariant.py` (R1: approval-without-CI) and
  `verify_implementer_invariant.py` (I1: head-commit fails commit-lint,
  I2: PR description missing Epic reference). Strictly additive cron-job-
  shaped scripts that open idempotent `auto/invariant-violation` issues;
  safe to run every 15 minutes in production.

- New `implementer-helpers` skill at
  `.opencode/skills/implementer-helpers/SKILL.md` + CLI at
  `tools/implementer_validate.py` (4 subcommands:
  validate-commit-message, validate-pr-compliance, validate-file-budget,
  validate-changelog). Mirrors the reviewer side; `tools/_commit_lint.py`
  is shared so a future change to commit policy updates one place.

- `auto-agents.md` watchdog gate: `DISPATCHERS_RUNNING=1` puts the
  primary orchestrator into watchdog-only mode. Heartbeat resolution +
  age computation factored into `tools/_watchdog_helpers.py` + the CLI
  `tools/watchdog_check.py` so the agent only needs
  `python3 tools/watchdog_check.py *` and `sleep *` bash permissions.
  The reader honours the env-var override first, then falls back to a
  freshest-mtime scan across `/var/run` / `$XDG_RUNTIME_DIR` / `/tmp`
  (deliberately diverging from the dispatcher's first-existing fallback
  to guard against stale heartbeats from previous root-owned sessions
  masking healthy user-mode heartbeats).

- `_opencode_worker.py` audit: structured `error_kind` classification
  at every transport-error / timeout return site, plumbed through
  `_dispatch_runtime.py` into the cycle-log; new `_request_read` retry
  helper (3 × 0.5s linear backoff, transport-only) wrapping every
  idempotent read in a worker session so a single transient flap on a
  polling GET cannot trash a 10-minute worker session.

- Static heredoc lint at `tests/auto_agents/test_prompt_heredoc_lint.py`
  glob-walks every agent prompt and skill recipe markdown, rejecting any
  heredoc bash recipe in a fenced code block (per `bash-commands.md`
  rule 2 — heredocs fail at OpenCode's permission-engine parse time).

- `bash-commands.md` rule 2 + its fix-it advice both lead with
  apostrophe-safe `printf "%s" "<body>"` (double-quoted) form;
  single-quoted form documented as the fragile JSON-only fallback.

- `CHANGELOG.md` carries the full multi-round narrative (round 3
  CRITICAL/HIGH/MEDIUM/LOW fold-in, round 5 docstring drift +
  telemetry refactor + broader heredoc lint scope, post-round-5
  doc-drift polish).

Net delta: +911 passing tests / 3 skipped (was 825 / 3); ruff clean
on every new file; pre-existing lint debt in `_dispatch_runtime.py`,
`_opencode_worker.py`, `conftest.py`, `_commit_lint.py` unchanged
and out of scope for this commit.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 11:48:12 -04:00

415 lines
15 KiB
Python

#!/usr/bin/env python3
"""Continuous implementer-pipeline invariant verifier.
Audits open PRs against the deterministic invariants the implementer
pipeline owns and opens ``auto/invariant-violation`` issues on
breach. Strictly additive — does not write any other state, and the
issues it opens carry a verifier-specific marker so a future re-run
is idempotent.
The implementer pipeline (``tools/dispatch_implementer.py`` + the
``task-implementor`` agent + the ``implementer-helpers`` skill)
catches PR Compliance Checklist failures before the worker exits.
This verifier catches the residual class: PRs where the worker
bypassed every pre-check (or a human-authored PR landed without
following the checklist) and the dispatcher's PR Compliance
Checklist invariants quietly drifted. Scope:
- **I1: Head commit fails commit-lint**. The PR's head commit
message fails ``_commit_lint.lint_commit_message`` with
``is_head=True`` (conventional-commit subject prefix or missing
``ISSUES CLOSED: #N`` footer). Bot-author exemption applies — a
merge-bot commit on the head is not a violation.
- **I2: PR description missing Epic reference**. The PR body has
no ``Epic: #N`` / ``Parent: #N`` line (per the dispatcher's PR
Compliance Checklist item 6 + the ``implementer-helpers``
skill's ``validate-pr-compliance`` regex).
This verifier does NOT police claim-label staleness — that is the
merge driver's per-cycle ``sweep_expired_claims`` job. It does NOT
police file-budget / CHANGELOG drift either; those require pulling
the diff from a worktree, which is heavyweight for a 15-min cron
job and is already enforced by reviewer feedback + CI.
Usage::
python3 tools/verify_implementer_invariant.py
python3 tools/verify_implementer_invariant.py --mode observe
python3 tools/verify_implementer_invariant.py --format json
python3 tools/verify_implementer_invariant.py --dry-run
Exit codes:
- ``0`` — verifier ran, no violations detected.
- ``1`` — verifier ran, ≥ 1 violation detected.
- ``2`` — verifier could not run.
"""
from __future__ import annotations
import argparse
import json
import logging
import os
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 load_sibling as _load_sibling # noqa: I001, E402 type: ignore[import-not-found]
_common = _load_sibling("_verify_common", "_verify_common.py")
_commit_lint = _load_sibling("_commit_lint", "_commit_lint.py")
_implementer_validate = _load_sibling(
"implementer_validate", "implementer_validate.py"
)
logger = logging.getLogger("verify_implementer_invariant")
# Verifier-specific marker so issues do not collide with other
# auto-agents verifiers.
ISSUE_BODY_MARKER = "<!-- auto-agents: implementer-invariant-violation -->"
def _configure_logging() -> None:
env_level = os.environ.get("VERIFY_INVARIANT_LOG_LEVEL")
level = (
getattr(logging, env_level.upper(), logging.INFO)
if env_level
else logging.INFO
)
if not logging.getLogger().handlers:
logging.basicConfig(
level=level,
format="%(asctime)s %(name)s %(levelname)s %(message)s",
stream=sys.stderr,
)
else:
logging.getLogger().setLevel(level)
# ─── Per-invariant helpers ────────────────────────────────────────────────
def fetch_head_commit(token: str, sha: str) -> dict[str, Any] | None:
"""Return the Forgejo commit object for ``sha``. ``None`` on any
HTTP error so the caller can skip the audit gracefully (the
worst case is a single missed audit, replayed next cron run)."""
res = _common.api(
"GET",
f"/repos/{_common.REPO_OWNER}/{_common.REPO_NAME}/git/commits/"
f"{sha}",
token,
)
if res["status"] != 200 or not isinstance(res["body"], dict):
return None
return res["body"]
def audit_commit_lint(
token: str,
pr: dict[str, Any],
*,
telemetry: dict[str, int] | None = None,
) -> dict[str, Any] | None:
"""Apply ``_commit_lint.lint_commit_message`` to the PR's head
commit. Returns a violation dict when the head commit fails the
lint (and is not bot-exempt); ``None`` otherwise.
When the head commit cannot be fetched (Forgejo transient error)
AND ``telemetry`` is provided, ``telemetry['skipped_due_to_transient_error']``
is incremented so the run-level summary can distinguish "audited
OK" from "could not audit". The audit returns ``None`` either
way — the worst case is a single missed audit replayed on the
next cron tick.
"""
head_sha = (pr.get("head") or {}).get("sha") or ""
if not head_sha:
return None
commit = fetch_head_commit(token, head_sha)
if commit is None:
if telemetry is not None:
telemetry["skipped_due_to_transient_error"] = (
telemetry.get("skipped_due_to_transient_error", 0) + 1
)
return None
message = commit.get("message", "") or ""
committer = commit.get("committer") or {}
committer_email = committer.get("email", "") or ""
result = _commit_lint.lint_commit_message(
message, committer_email, is_head=True
)
if result.get("ok") or result.get("exempt_bot"):
return None
return {
"kind": "commit-lint",
"pr_number": int(pr.get("number") or 0),
"head_sha": head_sha,
"committer_email": committer_email,
"subject": (message.splitlines() or [""])[0],
"violations": result.get("violations", []),
}
def audit_pr_compliance(pr: dict[str, Any]) -> dict[str, Any] | None:
"""Apply the same Epic-reference regex
:data:`implementer_validate.EPIC_REFERENCE_RE` uses to the PR
body. Returns a violation dict when no Epic reference is present;
``None`` otherwise. The two modules import the same compiled
regex object so the dispatcher's draft-time helper and the
continuous verifier can never disagree.
"""
body = pr.get("body", "") or ""
if _implementer_validate.EPIC_REFERENCE_RE.search(body):
return None
return {
"kind": "pr-compliance",
"pr_number": int(pr.get("number") or 0),
"head_sha": (pr.get("head") or {}).get("sha") or "",
"rule": "epic-reference",
"details": (
"PR description has no 'Epic: #<n>' or 'Parent: #<n>' "
"line on its own. Implementer-helpers' "
"validate-pr-compliance subcommand catches this at "
"draft-time; a violation here means the worker bypassed "
"the helper or a human PR landed without following the "
"PR Compliance Checklist."
),
}
def audit_one_pr(
token: str,
pr: dict[str, Any],
*,
telemetry: dict[str, int] | None = None,
) -> list[dict[str, Any]]:
"""Audit one PR for every implementer invariant. Returns a list
(possibly empty) of violation dicts. Drafts are skipped.
Predicates are listed as ``(name, callable)`` tuples so adding a
third invariant is one row, not three lines of cut-and-paste.
Each predicate either returns ``None`` (clean) or a violation
dict. The ``telemetry`` dict is forwarded only to predicates
that make remote API calls (currently just ``commit-lint``);
pure-data predicates ignore it.
"""
if pr.get("draft", False):
return []
predicates: tuple[
tuple[str, Any], ...
] = (
("commit-lint", lambda: audit_commit_lint(
token, pr, telemetry=telemetry
)),
("pr-compliance", lambda: audit_pr_compliance(pr)),
)
out: list[dict[str, Any]] = []
for _name, predicate in predicates:
violation = predicate()
if violation is not None:
out.append(violation)
return out
def _violation_payload(violation: dict[str, Any]) -> tuple[str, str, dict[str, Any]]:
"""Build ``(title, summary, body_meta)`` for one violation."""
pr_number = violation["pr_number"]
if violation["kind"] == "commit-lint":
title = (
f"Implementer invariant violation on PR #{pr_number}: "
f"head commit fails lint"
)
rules = ", ".join(
v.get("rule", "") for v in violation["violations"]
)
summary = (
f"PR #{pr_number}'s head commit `{violation['head_sha']}` "
f"fails commit-lint rules: {rules}. The "
f"`implementer-helpers` skill's `validate-commit-message` "
f"subcommand catches this at draft-time; a violation here "
f"means the worker bypassed the helper or a human commit "
f"landed without following CONTRIBUTING.md.\n\n"
f"Subject: `{violation['subject']}`\n"
f"Committer: `{violation['committer_email']}`"
)
body_meta = {
"invariant": "implementer-commit-lint",
"pr_number": pr_number,
"head_sha": violation["head_sha"],
"subject": violation["subject"],
"committer_email": violation["committer_email"],
"violations": violation["violations"],
}
elif violation["kind"] == "pr-compliance":
title = (
f"Implementer invariant violation on PR #{pr_number}: "
f"missing Epic reference"
)
summary = violation["details"]
body_meta = {
"invariant": "implementer-pr-compliance",
"pr_number": pr_number,
"head_sha": violation["head_sha"],
"rule": violation["rule"],
}
else:
# Defensive: shouldn't happen unless a future invariant is
# added without updating this branch.
raise RuntimeError(f"unknown violation kind: {violation['kind']!r}")
return title, summary, body_meta
def _classify_action(result: dict[str, Any]) -> str:
"""Map :func:`_common.open_violation_issue`'s return dict to a
cycle-log action label. Mirrors
:func:`verify_review_invariant._classify_action` so both verifiers
surface dry-run, success, and failure paths consistently.
"""
if result.get("created"):
return "issue_opened"
if result.get("issue") is not None:
return "issue_already_open"
if result.get("dry_run"):
return "would_create_issue"
return "issue_creation_failed"
def run(token: str, *, mode: str, dry_run: bool) -> dict[str, Any]:
"""Top-level audit loop. Iterates open PRs, audits each, and (in
``alert`` mode) opens an issue per violation.
The returned summary always includes
``skipped_due_to_transient_error`` so the cron caller (and the
cycle-log emitter) can pivot on "no violations because everything
audited cleanly" vs "no violations because we couldn't audit
some PRs at all".
"""
audited = 0
telemetry: dict[str, int] = {"skipped_due_to_transient_error": 0}
violations: list[dict[str, Any]] = []
actions: list[dict[str, Any]] = []
for pr in _common.iterate_open_prs(token):
audited += 1
for v in audit_one_pr(token, pr, telemetry=telemetry):
violations.append(v)
title, summary, meta = _violation_payload(v)
# Identifier is keyed on (pr#, kind, head_sha) so a single
# PR with both a commit-lint AND a pr-compliance violation
# gets two distinct issues, but a re-run does not duplicate
# them.
identifier = (
f"pr#{v['pr_number']}@{v['head_sha']}#{v['kind']}"
)
if mode == "observe":
logger.warning(
"implementer invariant violation (observe-only): %s",
json.dumps(meta),
)
actions.append(
{"identifier": identifier, "action_taken": "observed"}
)
continue
result = _common.open_violation_issue(
token,
title=title,
body_meta=meta,
marker=ISSUE_BODY_MARKER,
identifier=identifier,
summary=summary,
dry_run=dry_run,
)
actions.append(
{
"identifier": identifier,
"action_taken": _classify_action(result),
"result": result,
}
)
return {
"audited_prs": audited,
"violations": violations,
"actions": actions,
"violation_count": len(violations),
"skipped_due_to_transient_error": telemetry[
"skipped_due_to_transient_error"
],
}
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="verify_implementer_invariant",
description=(
"Continuous implementer-pipeline invariant verifier. "
"Audits open PRs against the dispatcher's PR Compliance "
"Checklist and opens auto/invariant-violation issues on "
"breach."
),
)
parser.add_argument(
"--mode",
choices=("alert", "observe"),
default="alert",
help=(
"alert (default) opens issues; observe logs to stderr "
"only."
),
)
parser.add_argument(
"--format",
choices=("text", "json"),
default="text",
help="Output format for the verifier summary.",
)
parser.add_argument(
"--dry-run",
action="store_true",
help=(
"In alert mode, refuse to actually create issues; print "
"what would have been created."
),
)
return parser
def main(argv: list[str] | None = None) -> int:
_configure_logging()
parser = _build_parser()
args = parser.parse_args(argv)
try:
token = _common.load_token()
except RuntimeError as exc:
logger.error("could not load Forgejo token: %s", exc)
return 2
try:
summary = run(token, mode=args.mode, dry_run=args.dry_run)
except RuntimeError as exc:
# ``_common.api`` raises RuntimeError after exhausting its retry
# budget on transport-level failures. Translate that to exit
# code 2 ("verifier could not run") per the contract above.
logger.error("Forgejo API error during audit: %s", exc)
return 2
if args.format == "json":
print(json.dumps(summary, indent=2))
else:
print(
f"audited={summary['audited_prs']} "
f"violations={summary['violation_count']} "
f"skipped_transient="
f"{summary.get('skipped_due_to_transient_error', 0)} "
f"mode={args.mode}{' (dry-run)' if args.dry_run else ''}"
)
for action in summary["actions"]:
print(f" - {action['identifier']}: {action['action_taken']}")
return 1 if summary["violation_count"] > 0 else 0
if __name__ == "__main__":
raise SystemExit(main())