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.
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.
| Fact | Where it lives | Stability |
|---|---|---|
| what failed | the error class docstring | stable |
| what to do next | the class remediation attribute (new) | stable |
| what was needed | the raised message / detail | runtime |
| what was actually found | the raised message / detail | runtime |
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.
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:
| Test | Proves |
|---|---|
test_gate_rejects_a_bare_code_class | a code-only class is flagged for BOTH missing docstring and missing remediation |
test_gate_reaches_subclasses_of_repo_error_classes | a mute subclass of a compliant repo-local base is still caught (the fixpoint) |
test_gate_rejects_a_blank_remediation | declaring remediation = "" does not satisfy it |
test_every_error_class_meets_the_contract | green 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.
remediation: 17 in
atlas/errors.py (incl. the 4-ledger memory family), 7 in
registrar/errors.py, plus CommandVocabularyError,
RecoveryError, both CatalogErrors, both
FrameBuildFaileds (Vega + Mira), RosterClashError,
CriticThresholdError, GovernThresholdError,
ToolPolicyError, GateRuleError, BudgetStopError,
BudgetExceededError.registrar.yaml loaders (govern.py, critic.py,
tool_policy.py, evals/gate_rule.py) now name the resolved file path
and what they found; atlas/memory/scope.py now names which segment
failed.remediation surfaced in operator logs —
atlas_run_failed and registrar_run_failed carry it as a field, so
debugging Step 2a reads "what to do" straight off the line.| Site | Before | After |
|---|---|---|
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." |
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.
| Check | Result |
|---|---|
ruff check tests/ scripts/ agents/ | All checks passed |
| gate red before the sweep | 36 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/tests | 507 passed |
pytest agents/registrar/tests | 290 passed |
pytest agents/mira/tests | 161 passed |
pytest agents/framer/tests | 120 passed |
pytest agents/context_judge/tests | 16 passed |
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.agent_event_stream.v1 is frozen; remediation stays
off the wire