> ## Documentation Index
> Fetch the complete documentation index at: https://codexceed.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# avatar.planner

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 &lt;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).

```python theme={null}
VerificationPlanner(config: HarnessConfig | None = None, client: Any = None) -> None
```

#### `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 `&#123;"code", "content"&#125;` 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 &lt;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.
