Story-2184 Walkthrough

Change: the agents mirror of pramaan-functions Story-2183 — enforce the "errors explain themselves" message contract (pramaan-spec Chore-2182) with a CI gate over every error class in agents/, then sweep the boundary errors humans and agents actually read when debugging.

Why this exists. The Mira ws-handler incident burned a day on a log line that said only websocket_request_rejected. A code with no message tells an operator nothing: not what was needed, not what was found, not what to do. The debugging playbook's Step 2a says "extract the structured error field from the matched log line" — that step is worthless if the field is mute. This gate makes a mute error class un-shippable.

The contract — four facts, split by how often they change

FactWhere it livesStability
what failedthe error class docstringstable
what to do nextthe class remediation attribute (new)stable
what was neededthe raised message / detailruntime
what was actually foundthe raised message / detailruntime

The gate — static (AST), not a subclass walk

pramaan-functions walks one PramaanError.__subclasses__() tree. This repo cannot, and the difference is architectural, not stylistic: each agents/<agent>/ is an independent uv workspace member and an independent CodeZip unit with a flat top-level module namespace (from errors import …). Agents may not import one another, so there is no shared root class to walk — and importing all five brains into one pytest process collides on models, config, provider, CatalogError and FrameBuildFailed.

So tests/test_error_contract_gate.py parses instead — the same static-governance shape as the existing tests/test_brain_egress_check.py. It walks every shipped module under agents/ (agent test suites excluded: a fixture error class is never read by an operator), finds every error class via a small fixpoint, and fails on a thin docstring or a missing/blank remediation.

def error_contract_violations(source: str, *, path: str = "<inline>") -> dict[str, list[str]]:
    """Contract violations for every error class in one module's source."""

The fixpoint matters: AtlasMemoryDisabledError inherits AtlasMemoryError, not a builtin. A gate that only matched class X(Exception) would silently skip the whole memory family.

Red proven, not assumed

Three of the four gate tests prove the gate has teeth, against inline source — so no mute class ever ships in agents/ just to demonstrate the failure:

TestProves
test_gate_rejects_a_bare_code_classa code-only class is flagged for BOTH missing docstring and missing remediation
test_gate_reaches_subclasses_of_repo_error_classesa mute subclass of a compliant repo-local base is still caught (the fixpoint)
test_gate_rejects_a_blank_remediationdeclaring remediation = "" does not satisfy it
test_every_error_class_meets_the_contractgreen across all five agents

On first run against main the gate went red on 36 classes — every error class in the repo, across Atlas, Blake, Mira, Vega and context_judge. Docstrings were already substantive everywhere; not one class carried a remediation. The gate governs the whole tree, not a curated list.

The sweep — messages and remediation only; codes untouched

Before / after — 3 exemplars

SiteBeforeAfter
atlas/memory/scope.py _seg() raise AtlasMemoryScopeError("empty namespace segment") — which segment? firm? user? matter? _seg(value, *, field); raises namespace segment 'scope.matter_id' must be a non-blank identity id, got ''. Root-cause fix in the shared helper — all 6 call sites named, not just the one that paged us.
registrar/govern.py threshold load "registrar.yaml is missing a 'confidence:' block" — no path, no found-value; which of the three checked-out registrar.yaml files? f"{MANIFEST_PATH} must define a 'confidence:' mapping of §8 decision bars; found {type(conf).__name__}", and the missing-keys branch names which keys are missing
atlas/errors.py AtlasRefusalFloorError docstring only remediation = "Run the adversarial suite to clear the D-5 refusal floor for this model, or select a model that already clears it in atlas/model.json. Do NOT disable floor enforcement, and never auto-downgrade to a cheaper model to dodge the floor."

No behavior change

Every code string is untouched — they are a frozen wire contract the app keys UI copy off. remediation is not added to agent_event_stream.v1 (frozen, pramaan-docs ADR-110 §3.2); it rides the operator log only. The one signature change, _seg(value) → _seg(value, *, field), is a private module helper with all call sites in the same file.

Verification — offline

CheckResult
ruff check tests/ scripts/ agents/All checks passed
gate red before the sweep36 classes flagged (proved, then fixed)
pytest tests/ (governance)179 passed (175 baseline + 4 new gate tests)
gate against a reverted agents/ tree (git stash)1 failed — the gate genuinely goes red on the pre-sweep code, it is not a rubber stamp
pytest agents/atlas/tests507 passed
pytest agents/registrar/tests290 passed
pytest agents/mira/tests161 passed
pytest agents/framer/tests120 passed
pytest agents/context_judge/tests16 passed
Live evidence: none, by design. This PR ships no runtime behavior. The only live-observable delta is a new remediation field on the atlas_run_failed / registrar_run_failed log lines, which appears the next time a brain fails after deploy. Nothing here needs an AgentCore invoke to prove — and per the runtime-liveness doctrine (Chore-2190), green CI is not a liveness claim, so none is made.

Doctrine trail