# Template: .atk/runner/failure_rule.py

Copy this template into `.atk/runner/failure_rule.py` when using the `atk-init-failure-rule` Skill. Replace every `TODO_AGENT_TUNING` block with the confirmed rule logic. The `atk-find-failures-by-rule` Skill executes this script later.

```python
#!/usr/bin/env python3
"""Rule-based failure-case filter for Agent Tune Kit.

Generated by the atk-init-failure-rule Skill.
Reads current .atk/results/vN/eval_results.csv and overwrites current
.atk/results/vN/failure_cases.csv.
"""

from __future__ import annotations

import csv
from pathlib import Path
from typing import Dict, List

RESULTS_DIR = Path(".atk/results")
RESULTS_FILENAME = "eval_results.csv"
FAILURE_FILENAME = "failure_cases.csv"


class UserActionRequired(RuntimeError):
    """Raised when the user must repair inputs or confirm an unsafe inference."""


def list_version_dirs(results_dir=RESULTS_DIR):
    # Return [(number, path)] for directories named vN where N is a positive integer.
    # Missing results_dir means no versions exist yet; runner first run will create v1.
    if not results_dir.exists():
        return []
    return sorted(
        (int(path.name[1:]), path)
        for path in results_dir.iterdir()
        if path.is_dir() and path.name.startswith("v") and path.name[1:].isdigit() and int(path.name[1:]) > 0
    )


def resolve_current_version(results_dir=RESULTS_DIR):
    # Used by every non-runner Skill. Do not filter by required files.
    versions = list_version_dirs(results_dir)
    if not versions:
        raise UserActionRequired("No vN results directory exists; run eval_runner.py first or confirm repair.")
    return versions[-1][1]


def require_current_file(current_dir, filename):
    # Missing inputs are blockers; never fall back to an older version.
    path = current_dir / filename
    if not path.exists():
        raise UserActionRequired(f"Current version {current_dir.name} is missing {filename}; fix or rerun the prior step.")
    return path


def load_results(path: Path) -> tuple[List[str], List[Dict[str, str]]]:
    with path.open("r", newline="", encoding="utf-8-sig") as handle:
        reader = csv.DictReader(handle)
        if not reader.fieldnames:
            raise UserActionRequired(f"eval_results.csv has no header row: {path}")
        fieldnames = list(reader.fieldnames)
        if "agent_output" not in fieldnames:
            raise UserActionRequired("eval_results.csv is missing required agent_output column.")
        return fieldnames, list(reader)


def is_failure(row: Dict[str, str]) -> bool:
    """Return True when a row should be written to failure_cases.csv.

    TODO_AGENT_TUNING: replace with confirmed rule logic before running.
    Examples:
    - return row.get("expected") != row.get("agent_output")
    - return "error" in row.get("agent_output", "").lower()
    - parse JSON in agent_output and check a structured field.
    """
    raise UserActionRequired("TODO_AGENT_TUNING: implement confirmed failure rule before running.")


def write_failures(path: Path, fieldnames: List[str], rows: List[Dict[str, str]]) -> None:
    with path.open("w", newline="", encoding="utf-8") as handle:
        writer = csv.DictWriter(handle, fieldnames=fieldnames, extrasaction="ignore")
        writer.writeheader()
        writer.writerows(rows)


def main() -> int:
    current_dir = resolve_current_version()
    results_path = require_current_file(current_dir, RESULTS_FILENAME)
    fieldnames, rows = load_results(results_path)
    failure_rows = [row for row in rows if is_failure(row)]
    failure_path = current_dir / FAILURE_FILENAME
    write_failures(failure_path, fieldnames, failure_rows)
    print(f"Read {results_path}")
    print(f"Overwrote {failure_path} with {len(failure_rows)} failure rows")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
```
