Files
cleveragents-core/tools/_diff_aware_gate.py
T
drew b154d48027 feat(auto-agents): deterministic worker-side improvements
Four changes that move judgment off the LLM and into the dispatcher,
motivated by the live PR #30 escalation pilot (2026-05-12): Tier 0
spent 16 min and gave up on flaky-looking unrelated tests; Tier 2
spent 88 min discovering the PR was already correct and only needed
two compliance entries. Together these collapse the typical
"PR is correct, only needs compliance fixups" case from 88 min on
Kimi to ~5 min on gpt-5-mini at Tier 0.

- Outcome-JSON synthesis in dispatch_implementer
  (_synthesize_outcome_if_missing): synthesises a concrete outcome
  from terminal_state when the worker emits no contract JSON,
  routing the escalation predicate to ESCALATE instead of UNKNOWN.

- Diff-aware gate parser (_diff_aware_gate.py): pure-Python
  classifier that splits failing BDD scenarios into related vs.
  unrelated to the PR's changed files via a feature-stem heuristic.

- Flaky-test pre-flight (_implementer_gate_preflight.py): runs
  local_ci_gate.sh --fast twice and surfaces only the persistent
  failures. Off-by-default behind IMPLEMENTER_DISPATCHER_GATE_PREFLIGHT.

- Compliance gap detector (_implementer_compliance.py): deterministic
  check of CHANGELOG / CONTRIBUTORS / commit-footer / worktree-clean
  state. Result is embedded as a "Compliance gap report" stanza so
  the agent fills in known gaps instead of discovering them.

Both prompt stanzas are appended via _append_deterministic_stanzas,
flag-gated, skipped in dry-run, and skipped when no preclone exists.
Flag-off path is byte-equivalent to the pre-feature build.

Suite: 1382 passed, 3 skipped (+59 new tests over 4 modules).

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

258 lines
9.8 KiB
Python

"""Diff-aware classifier for ``local_ci_gate.sh`` quality-gate
output.
Cross-references the failing BDD scenarios reported by the gate
runner against the PR's changed-files list, splitting failures
into two buckets:
- ``related_failures`` — failures in feature files whose paths
match (or are colocated with) files the PR touches. Almost
always real regressions caused by the PR.
- ``unrelated_failures`` — failures in feature files the PR did
not touch. Strongly correlated with environmental flakiness or
CI-side intermittency — these are the failures Tier 0 historically
bailed out on (e.g., PR #30 Run 1, 2026-05-12, where six CLI
scenarios failed unrelated to the LangGraph fix and gpt-5-mini
gave up rather than re-running them).
Used by the dispatcher's pre-flight pre-fetch path (Heavy flaky
pre-flight, plan §"#1 Heavy") to verify a failure is persistent
before surfacing it to the worker. Also useful as a standalone
helper the implementer worker can call mid-session.
This module is pure-Python and has no I/O of its own — callers
provide the gate output text and the list of changed files. Easy
to unit-test.
"""
from __future__ import annotations
import re
from typing import Iterable
# Pattern for failing-BDD-scenario lines in ``behave`` output:
#
# features/path/to/file.feature:42 Scenario: did the thing
#
# behave reports failures as "Failing scenarios:" header followed by
# indented ``path:line`` lines. The trailing scenario name is
# optional and may contain anything; we only capture the file path
# and line number which are the load-bearing identifiers.
_BEHAVE_FAILURE_RE = re.compile(
r"^\s*(?P<path>features/[^\s:]+\.feature):(?P<line>\d+)",
re.MULTILINE,
)
# When a gate reports "FAILED" via the local_ci_gate.sh wrapper the
# header line looks like ``CI / unit_tests* FAIL ...``. Used as a
# coarse status indicator for gates the parser doesn't deep-parse.
_GATE_STATUS_RE = re.compile(
r"^(?P<gate>lint|typecheck|unit_tests|integration_tests|"
r"e2e_tests|coverage)\s*:\s*(?P<status>PASS|FAIL|SKIP)",
re.MULTILINE | re.IGNORECASE,
)
def parse_failing_scenarios(gate_output: str) -> list[dict[str, str]]:
"""Extract failing scenario paths + line numbers from gate output.
Returns a list of ``{"path": str, "line": str}`` dicts in the
order they appear in the output. Duplicates are de-duplicated
by ``(path, line)`` because a behave summary tends to list each
failure both inline and in the "Failing scenarios:" footer.
"""
seen: set[tuple[str, str]] = set()
out: list[dict[str, str]] = []
for m in _BEHAVE_FAILURE_RE.finditer(gate_output or ""):
key = (m.group("path"), m.group("line"))
if key in seen:
continue
seen.add(key)
out.append({"path": m.group("path"), "line": m.group("line")})
return out
def parse_gate_statuses(gate_output: str) -> dict[str, str]:
"""Map gate name → status (PASS/FAIL/SKIP) parsed from output.
Coarse-grained — used by the dispatcher to surface a high-level
gate roll-up in the worker prompt without exposing the full raw
output. The agent reads this dict and can decide whether to
drill into specific failures.
"""
out: dict[str, str] = {}
for m in _GATE_STATUS_RE.finditer(gate_output or ""):
out[m.group("gate").lower()] = m.group("status").upper()
return out
def _normalise_path(p: str) -> str:
"""Strip leading ``./`` and trailing whitespace from a path."""
return (p or "").strip().removeprefix("./")
def _feature_likely_related(
feature_path: str, changed_files: Iterable[str]
) -> bool:
"""Heuristic: a feature is likely related to a PR's changes if
EITHER the feature file itself was changed OR the feature's
name-stem appears in a changed source-file path.
Examples:
- feature ``features/auto_debug_cli_coverage.feature`` is related
to a PR touching ``src/cleveragents/auto_debug/cli.py`` (stem
``auto_debug`` appears in both).
- feature ``features/plan_cli_commands_r2.feature`` is unrelated
to a PR touching ``langgraph/graph.py`` (no overlap).
This is intentionally simple — false positives (calling
something related when it isn't) are cheap (we just don't
auto-suppress that failure). False negatives (calling something
unrelated when it IS) are also cheap because the worker still
sees the failure list and can decide.
"""
feature_path_norm = _normalise_path(feature_path)
changed_norm = {_normalise_path(c) for c in changed_files}
# Direct hit — feature file itself changed.
if feature_path_norm in changed_norm:
return True
# Extract the stem of the feature name (strip extension + path).
# ``features/auto_debug_cli_coverage.feature`` → ``auto_debug_cli_coverage``.
stem = feature_path_norm.split("/")[-1]
if stem.endswith(".feature"):
stem = stem[: -len(".feature")]
if not stem:
return False
# Split the stem into prefix tokens — the most-specific token
# first. We compare just the first 2-3 tokens because feature
# filenames are often verbose ("plan_cli_commands_r2_boost") and
# matching the WHOLE stem against a path would miss legitimate
# matches like ``plan_cli/commands.py``.
tokens = stem.split("_")
# Take the first 2 tokens as the "salient prefix" — the bit
# that's most likely to map to a source-tree path component.
prefix_candidates = []
if tokens:
prefix_candidates.append(tokens[0])
if len(tokens) >= 2:
prefix_candidates.append("_".join(tokens[:2]))
for prefix in prefix_candidates:
if not prefix:
continue
for changed in changed_norm:
# Match against PATH SEGMENTS — avoid matching substrings
# of unrelated names (``plan`` matches ``plans/`` but
# not ``misplanned``).
segments = changed.replace(".", "/").split("/")
if prefix in segments:
return True
return False
def classify_failures(
gate_output: str, changed_files: Iterable[str]
) -> dict[str, object]:
"""Top-level classifier: parse + relate.
Returns a structured dict the dispatcher embeds in the worker
prompt:
.. code-block:: python
{
"gate_statuses": {"lint": "PASS", "unit_tests": "FAIL", ...},
"failures_total": 6,
"failures_related_to_diff": 0,
"failures_unrelated_to_diff": 6,
"related": [{"path": ..., "line": ...}, ...],
"unrelated": [{"path": ..., "line": ...}, ...],
}
The agent's instruction (in implementation-worker / task-implementor
prompts): if ``failures_related_to_diff == 0`` and the gate
pre-flight already retried persistent failures, the run is
PROBABLY flaky — focus on compliance gaps / fill-the-blanks
rather than diagnosing the unrelated failures.
"""
statuses = parse_gate_statuses(gate_output)
failures = parse_failing_scenarios(gate_output)
changed = list(changed_files)
related: list[dict[str, str]] = []
unrelated: list[dict[str, str]] = []
for f in failures:
if _feature_likely_related(f["path"], changed):
related.append(f)
else:
unrelated.append(f)
return {
"gate_statuses": statuses,
"failures_total": len(failures),
"failures_related_to_diff": len(related),
"failures_unrelated_to_diff": len(unrelated),
"related": related,
"unrelated": unrelated,
}
def render_prompt_stanza(classification: dict[str, object]) -> str:
"""Render the classification result as a markdown stanza for the
worker prompt. Kept here (next to the classifier) so the
rendered shape stays lockstep with the data shape.
Returns the empty string when there are zero failures to surface
— the dispatcher embeds the stanza unconditionally when the
classification ran, but the agent reads "empty stanza = all
gates passed in pre-flight" without a special-case.
"""
total = classification.get("failures_total", 0)
related = classification.get("related") or []
unrelated = classification.get("unrelated") or []
statuses = classification.get("gate_statuses") or {}
if not total and not statuses:
return ""
lines = ["## Pre-flight quality gates (verified persistent)\n"]
if statuses:
lines.append("Gate summary:")
for gate, status in sorted(statuses.items()):
lines.append(f"- `{gate}`: **{status}**")
lines.append("")
if total == 0:
lines.append(
"_No failures persisted after the pre-flight retry — "
"you do NOT need to re-run `local_ci_gate.sh --fast` "
"yourself unless you change files._"
)
return "\n".join(lines)
lines.append(
f"Failures total: **{total}** "
f"(related to your diff: {len(related)}, "
f"unrelated: {len(unrelated)})."
)
lines.append("")
if related:
lines.append("**Related to your diff** — investigate these:")
for f in related:
lines.append(f"- `{f['path']}:{f['line']}`")
lines.append("")
if unrelated:
lines.append(
"**Unrelated to your diff** — these were retried by "
"the dispatcher's pre-flight and persisted, but they "
"exercise code your PR did not touch. Likely "
"environmental / pre-existing. Do NOT bail on the "
"cycle for these — focus on the related failures "
"above (if any) and on compliance gaps (next section). "
"If you have time, you may re-run these locally to "
"confirm reproducibility before exiting."
)
for f in unrelated:
lines.append(f"- `{f['path']}:{f['line']}`")
return "\n".join(lines)