Classify (#1538) proposes a registration per document; Govern (#1539) gates new vocabulary; the eval gate-rule
(#1544) keeps every business mutation on suggest until proven. This Story adds the layer ABOVE the single
document: the batch run. The brain stays proposal-only and broker-mediated (pramaan-docs ADR-128) — Blake
aggregates and proposes the summary / review-routing / undo effects; the broker persists the card and
flips the effect ledger. No model call is involved: batch aggregation is deterministic Python over the per-document
turns the classify pass already produced, which is exactly what makes a re-run idempotent.
| Area | Where | What it does |
|---|---|---|
| The §5 fold | agents/registrar/batch.py | Deterministic aggregator (mirrors govern.py): folds per-document turns into the counts table + exception clusters + partial-failure note. Pure, offline, no model. |
| Typed contracts | agents/registrar/models.py | RegistrarDocumentResult, RegistrarBatchCounts, BatchExceptionCluster, RegistrarBatchSummary, the undo input/output, and AgentEffectRecord (the ledger row Blake reasons over). |
| Broker intents | agents/registrar/policy.py | intents_for_batch_summary + intents_for_undo_batch, and _system_effect_policy — the group-G exemption that lets a system effect ship auto while a business mutation never can. |
| Orchestrators | agents/registrar/agent.py | summarize_batch / undo_batch — span-wrapped, deterministic, assemble the output + its intents. |
| Runtime modes | agents/registrar/runtime.py | New summarize / undo_batch modes emit honest agent_event_stream.v1 frames (run.started → model.usage → turn.completed → run.completed), zero tokens. |
| Tests | tests/test_registrar_batch.py | 19 offline tests: the 120-file card, cluster-not-per-file, privilege-always-reviewed, partial-failure visible, idempotency, undo reverses only reversible auto effects, custody never touched, runtime frames. NO key. |
A 120-file upload yields exactly one RegistrarBatchSummary. The counts are a per-document fold;
new_document_types / new_filing_types count distinct proposed values (one per cluster),
not documents, so the card reads "1 new filing type (9 docs)".
# batch.py — the deterministic fold (excerpt)
for result in results:
if _is_failed(result):
counts.failed += 1
continue
proposal = result.turn.proposal
if proposal is not None:
if proposal.display_name.strip(): counts.renamed += 1
if proposal.tags: counts.tagged += 1
if proposal.document_type: counts.typed += 1
if result.turn.duplicate_candidate: counts.duplicate_candidates += 1
if result.turn.privilege_candidate: counts.possible_privileged += 1
if result.turn.verdict == "needs_review": counts.low_confidence += 1
write_registration_summary is keyed by batch_id so the broker upserts ONE card.The card surfaces only the exceptions needing counsel judgment, grouped so counsel acts once. New vocabulary
clusters by proposed label (batch_confirm — a single one-click approve); privilege, duplicate, and
low-confidence cluster by flag (human_review).
| Cluster kind | Resolution | Counsel action |
|---|---|---|
new_filing_type | batch_confirm | "Found 9 Reply Affidavits — add?" — one click for the whole cluster. |
new_document_type | batch_confirm | Confirm the coined matter-provisional type once. |
possible_privileged | human_review | Always to a human — never auto-set, never auto-cleared. |
duplicate_candidate | human_review | Surfaced for merge/keep — dedup never destroys. |
low_confidence | human_review | Routed to review rather than guessed. |
set_privilege is a forbidden tool).autoThe classify/govern intent path (_policy_for) hard-refuses any auto intent: business
mutations stay suggest until their eval gate flips them (#1544). The batch effects are different — they are
group-G system effects (Blake's own ledger/status writes, not firm vocabulary), which the gate-rule exempts.
# policy.py — the exemption is narrow, and fails loud if abused def _system_effect_policy(tool): policy, system_effect = _load_system_effect_table()[tool] if policy == "auto" and not system_effect: raise ToolPolicyError(...) # auto is NEVER a back door for a business mutation return policy
So write_registration_summary / create_registrar_review_item /
complete_registration_batch ship auto (system effects), and undo_agent_effect
ships gated (counsel-initiated). The underlying applies — names, tags, types — are untouched by this Story
and stay suggest.
Counsel can revert the whole batch. Undo reverses only the reversible auto_applied effects for a
batch_id; an effect that was never applied, is already undone, was confirmed by
counsel, is non-reversible, or names a custody tool is left in place.
# policy.py — the leash, restated as a filter reversible = (effect.state == "auto_applied" and effect.reversible and effect.tool not in _NEVER_UNDO_TOOLS) # set_privilege / mark_filed / delete_document …
undone → skipped). The custody guard is defense-in-depth: Blake never auto-applies a
final-privilege / filing-state / destructive effect, so those never reach auto_applied — but the filter
makes the boundary explicit and tested.Per-document isolation means one bad file doesn't sink the batch. Failures are counted and surfaced:
11 of 120 failed → failure_note = "11 failed, retrying", batch_status = "partially_done" all failed → batch_status = "failed" none failed → batch_status = "done"
$ cd agents/registrar && pytest tests -q 196 passed # 19 new batch tests + 177 prior, all offline, no key $ ruff check . # All checks passed! $ pytest tests/ -q -m unit # repo governance (incl. brain-egress) 101 passed
The brain-egress gate confirms the new code adds no DB/S3/SQS/HTTP dependency — Blake still writes no canonical truth and holds no credentials. The summary, review-routing, and undo are typed proposals the broker executes.