create_*, Blake runs three INDEPENDENT model lenses — Match · Taxonomy · Legal-validity — and a deterministic aggregator that applies the §8 thresholds + the §6 catalog-maturity regime. Default verdict is REJECT (bias to reuse). Coined types land as "proposed, pending approval"; Blake never coins firm-canonical vocabulary and can never approve his own proposal.The classify capability (#1538) assigns an existing type where the catalog has a credible match, and
deliberately leaves document_type null when none honestly fits — that gap routes here. This Story
fills the Govern capability: the closest-match panel that gates NEW controlled-vocabulary creation
(spec §8, pramaan-docs ADR-128 §2). It is the exception path, not every document — classify
handles the routine. Coined values are proposals the pramaan-functions broker executes under auth ·
firm/matter RLS · per-tool policy · idempotency · audit; Blake calls no tool, holds no credentials, and writes
no business truth.
| Area | Where | What it does |
|---|---|---|
| Lens + panel contracts | agents/registrar/models.py | MatchLensVerdict / TaxonomyLensVerdict / LegalValidityLensVerdict (forced structured, default-reject) · GovernRoute (the four routes) · GovernTargetKind (the strictness gradient) · RegistrarGovernInput · the rich RegistrarGovernOutput panel with the §6/§8 closest-match fields + votes + provisional lifecycle. |
| Three lens prompts | agents/registrar/prompts/govern.py | Three DISTINCT system prompts, no shared scratchpad — Match (reuse-bias), Taxonomy (anti-sprawl), Legal-validity (filing-only). Catalog + document body rendered inside the recursive evidence fence (spec §10.6). |
| Deterministic aggregator | agents/registrar/govern.py | aggregate_panel applies the §8 bars (reuse ≥ 0.82 · panel band 0.60–0.82 · create < 0.60 + proposal ≥ 0.85) + the §6 regime over the three verdicts → one of four routes. Thresholds read from registrar.yaml (config, not code). |
| Gated, idempotent intents | agents/registrar/policy.py | intents_for_govern emits the broker-mediated create_matter_document_type / request_firm_document_type / request_filing_type intents — never auto — each carrying an idempotency_key (create-race guard) + the provisional lifecycle args. |
| Orchestration + wire | agents/registrar/agent.py, runtime.py | run_govern runs the lenses required by the strictness gradient, aggregates, assembles the panel, and fails closed to the safe REJECT route. The runtime stamps each govern intent is_proposal+requires_broker_mediation+frozen source_refs. |
| Eval fixtures | agents/registrar/evals/govern_cases.jsonl | The four §12 anti-sprawl scenarios — agreement-vs-contract (Match) · force-fit-rejected (NoForceFit) · bootstrap-not-starved · near-dup-no-create — driven through the aggregator now; full model-judge gates land in #1544. |
| Tests | agents/registrar/tests/test_registrar_govern_capability.py | 38 offline tests: every route, the strictness gradient, the no-force-fit / no-near-dup / bootstrap-not-starved invariants, idempotency, fail-closed, the wire wrap, and the four fixtures. |
Each lens is its own Pydantic AI agent with a distinct prompt and no shared scratchpad (spec §8). The lenses report; the aggregator decides. Making the decision deterministic code (not a fourth LLM) is what makes the gate testable + calibratable and lets the eval gates assert exact behaviour.
# govern.py — aggregate_panel, the §8 bars + the §6 regime exact catalog dup -> reuse_existing # NoNearDupCreate, strongest match_confidence >= 0.82 -> reuse_existing # the reuse bias, when honest not a distinct category -> leave_unassigned_..review # a variant never force-fits / creates 0.60 <= match < 0.82 -> leave_unassigned_..review # show the closest-match panel match < 0.60 + proposal>=0.85 -> create_matter_provisional # Bootstrap/Growing only (BootstrapNotStarved) ...same, Mature catalog -> leave_unassigned_..review # no auto-create for a singleton ...same, filing_type -> request_firm_type # unanimous + legal-valid + HUMAN-gated otherwise -> leave_unassigned_..review # the safe REJECT default
leave_unassigned_needs_review — Blake never coins on doubt and never jams a doc into a near-but-wrong
type. But an empty catalog must be populated or Blake can't classify, so the create bar relaxes by catalog
maturity (§6): generous in Bootstrap, conservative in Mature. NoForceFit and
BootstrapNotStarved live on different axes (create vs reuse), so they never conflict.
| Target | Lenses run | Create fallback |
|---|---|---|
tag | Match only (lowest bar — may skip the full panel) | matter-local, low bar (cheap + reversible) |
document_type | Match + Taxonomy | matter-local provisional (regime-gated) pending approval |
filing_type | Match + Taxonomy + Legal-validity (required) | never matter-local — unanimous + human-gated firm request only |
A create route emits a single gated intent (the broker decides + executes), stamped with the provisional lifecycle and a deterministic idempotency key so two concurrent batches proposing the same value never double-create.
# policy.intents_for_govern — the gated create intent for a matter-local provisional RegistrarToolIntent( tool="create_matter_document_type", policy="suggest", # the LAUNCH policy — never "auto" (gate-rule, #1544) args={ "name": "Mediation Settlement", "matter_id": "matter-1", "provisional": True, "created_by": "blake-via-ledger", "review_state": "proposed", "source_doc_ids": ["doc-1"], "idempotency_key": "firm-1:matter-1:document_type:mediation settlement", })
is_proposal +
requires_broker_mediation + frozen source_refs ride the agent_event_stream.v1
turn. The broker holds the credentials and the gate-rule; it decides per effect and writes the catalog row under
RLS + audit. Promotion of a provisional to firm-canonical is a separate human admin action. Blake proposes; he
never mutates (pramaan-docs ADR-128).
38 govern tests 152 registrar suite 101 governance ruff clean brain-egress clean — Blake stays thin: no DB/credentials, OpenRouter+ZDR only, one event language. The four named eval fixtures run through the aggregator now; the full model-judge gate harness lands in the evals Story (#1544).