Skip to main content
VerificationPlanner — per-session verification-plan resolution (ADR-0007). The harness-owned collaborator that resolves what proves this repo’s work, once per session, before editing begins. Resolution is tiered:
  1. Config override (always wins): AVATAR_TEST_COMMAND / AVATAR_LINT_COMMAND, per slot. The user’s stated contract is never overridden.
  2. Deterministic detection (no LLM, no Python assumption): read repo artifacts — CI workflows, package.json, pyproject.toml, tox/nox, Cargo.toml, go.mod, .pre-commit-config.yaml, Makefile — and extract their declared test/lint invocations. CI-derived commands rank above arbitrary Makefile targets (CI is the gate the project actually trusts — least gameable).
  3. LLM fallback (evidence-grounded only, opt-in via AVATAR_PLANNER_MODEL): the model may propose a command for a slot detection left empty, but only citing the artifact it came from; the harness validates the citation before accepting. A proposal without valid provenance is rejected. No model configured → this tier simply does not exist (resolution stays deterministic and offline).
The resolved plan is a list of PlannedCheck (name, command, kind, provenance). The runner freezes it onto TaskState at the investigating → editing boundary and journals it; the Verifier then executes it with zero language knowledge. Python-ecosystem tools are emitted as python -m <tool> so an installed-but-not- on-PATH tool still resolves (the ADR’s robustness floor).

Classes

VerificationPlanner

Resolves the per-session verification plan (ADR-0007) — harness-owned. Args: config: Harness config; test_command/lint_command are the override tier and planner_model (unset by default) opts into the LLM fallback. client: An injected OpenAI-compatible client for the fallback, or None to build one lazily on first use (the ModeClassifier precedent; the fallback is never consulted unless planner_model is set).

VerificationPlanner.propose_smoke_check(self, ws: Workspace, files_modified: list[str]) -> PlannedCheck | None

Have the model author ONE executable smoke check for freshly-written code. The greenfield floor: used only when tiers 1-3 resolved nothing and the run wrote code, so there is genuinely no declared contract to discover. The model chooses which command; the harness still runs it and reads the real exit code, so this is author-and-run, never the self-certification §5 forbids. Resolved at verification time (the artifact under test does not exist at the freeze boundary). Runs on config.model (the main model) so the floor needs zero extra config; any endpoint failure degrades to “no floor”, never blocks. Args: ws: The run-scoped workspace whose just-written files are excerpted. files_modified: The repo-relative paths the run created or changed. Returns: The model-smoke check, or None when nothing runnable was proposed.

VerificationPlanner.resolve(self, ws: Workspace) -> list[PlannedCheck]

Resolve the verification plan for ws: override → detection → LLM fallback. Args: ws: The run-scoped workspace whose repo artifacts are read. Returns: The resolved checks, test slot first; empty when nothing is declared anywhere (the verifier then fails legibly — never an invented default).

Functions

check_covers_content(command: str) -> bool

Whether a declared check covers a content change: anchored + falsifiable (ADR-0044). The content rulebook replaces “must run what you build” (meaningless for a textual deliverable) with two demands, judged together per stage (_stage_inspects_content): anchored — some stage receives a content artifact (.md/.rst/.txt/.adoc, behavior-bearing manifests excluded) as a real operand, so a plain pytest check never silently “covers” the docs half of a mixed change and a quoted filename inside a code string anchors nothing; falsifiable — that stage asserts (an assertive inspector with an operand, or a real executor), and no trailing || alternative that cannot fail (|| true, || echo fine) neutralizes the line to exit 0. The stage split is quote-blind like vacuous_declared_check — live declared checks are single argvs by the time they reach here (the ADR-0045 gate rejects/splits operators first), so the multi-stage handling is defense-in-depth for direct callers, kept so the two layers can be edited independently. The immutable floor beneath the contract is the ultimate anchor. Args: command: The declared check command to classify. Returns: True when the command satisfies the content rulebook.

classify_change_paths(paths: Iterable[str]) -> set[Literal['code', 'content']]

The change kinds present in a set of changed paths (ADR-0044). The verification-time half of the declared-kind audit: .md/.rst/.txt/.adoc classify as content; every other suffix — and behavior-bearing .txt manifests (requirements*.txt, CMakeLists.txt) — classifies as code, so ambiguity fails toward the stricter rulebook. Args: paths: Workspace-relative changed paths (from the diff / files_modified). Returns: The subset of {"code", "content"} present; empty for no paths.

config_override_checks(config: HarnessConfig | None) -> list[PlannedCheck]

The override tier: checks declared explicitly in config (always win, per slot). Shared by the planner (tier 1) and the verifier’s no-plan fallback, so the two read the user’s stated contract identically. Args: config: The harness config, or None (no overrides). Returns: Up to one test and one lint check, for the non-empty config commands.

effective_invocation(command: str) -> tuple[str, list[str]]

The effective program + args of one command segment (program-position parse). Strips leading env-var assignments (CI=1 pytest) and unwraps runner wrappers (sudo, uv run, npx, python -m <module>) so classification keys on what actually executes — never on a token appearing anywhere in the line. Args: command: One command segment (no &&/; chaining). Returns: (program, args) — program is the basename (empty when nothing remains).

vacuous_declared_check(command: str) -> bool

Whether a model-declared verification command is vacuous (proves nothing — ADR-0038). A check is vacuous when it is empty or when every executed program on the line — each &&/||/; segment and each | pipeline stage, after unwrapping env/sudo/uv run/npx/python -m — is a no-op/inspector (true, echo, grep, …). Such a line exits 0 regardless of the artifact, so it can’t be a real contract. One real stage redeems the line: printf 'q' | python -m game drives the program under test via stdin and must not be rejected for its printf head (dogfood 7e49b161 — the rejection cost a turn plus a tier-3 amendment approval for an equivalent check). The split is quote-blind, so an in-quote |/; can only mis-split toward accepting — e.g. echo "a | b" now passes because its phantom second stage b" reads as an unknown (real) program. That is the safe direction for a lower-bound guard: the immutable floor beneath the declared contract is what ultimately anchors non-vacuity. Args: command: The declared check command to validate. Returns: True when the command is vacuous and must be rejected.