Mira learns to "do"

Story-2022 gave Mira a body but no voice. This Story gives her the command route — the "do" door: counsel selects a scope (one field, or the whole Brief) and types an instruction, and Mira runs Vega's exact proposer → critic → synthesizer panel over the current frame + that command, landing a proposed frame edit for Confirm/Reject. She never writes canonical — she proposes, counsel promotes.
Story-2027 (Story-C) · EPIC-2021 (Mira) · depends on Story-2022 (skeleton, merged) · reuses Vega's panel + frozen FrameOutput · pramaan-docs ADR-110 (thin brain / event stream) · ADR-117 (Pydantic AI) · ADR-151/116/120 (config-driven OpenRouter, ZDR) · repo pramaan-agents · design docs/notes/rfd-mira-matter-agent.md §2.2 / §4 / §9 Story-C

The stakes. A command rewrites the legal Brief — the source-true account every lawyer and every downstream agent relies on. So the command route cannot be a cheap single-model edit: it earns the panel's fabrication defences (diverse proposers make a confident hallucination visible as disagreement; the critic is the evidence accountant; the synthesizer owns the one final prose). The design (RFD §2.2) is deliberately near-zero new orchestration: Mira reuses the panel and adds only a thin command layer on top.

The naive fix and why it fails. "Just ask one model to edit the field." That trusts a model to change only what counsel scoped and to invent nothing — two things models do badly under a directive. Instead this Story leans on the machinery Vega already proved: the delta is confined to the scope in code (deterministic lock carry-through), not by prompt hope, and the content is audited by a distinct critic. The command is the steering; the locks are the guardrail.

1 · The one trick: scope → locks → deterministic carry-through

The whole route turns on one reuse. Vega's panel already has lock machinery (D-9): counsel can pin fields, and after synthesis every locked field is overwritten in code with its previous value — a model that argues a locked field should change simply cannot win. Mira's command route drives that machinery from the scope:

ScopeLocks (carried through unchanged)Result
<field> (e.g. theory_of_case)every FrameOutput field except the targetchanged_fields ⊆ {scope} — only that field can differ
"brief"the Constitution / identity tier (parties, forum, seat, governing_law, tribunal, case_ref, cause_title, counsel, instruments)identity holds; the fight + doctrine tiers regenerate
Why the guarantee is real, not aspirational: changed_fields ⊆ {scope} is not something the model is asked to respect — it is enforced by panel._carry_locked_fields, which reverts every out-of-scope field to the current frame's value after the model writes. The offline test scripts the synthesizer to maliciously change identity + the dispute on a theory_of_case command; the assertion is changed_fields == ["theory_of_case"]. The code wins.

2 · Reusing Vega's panel without breaking the CodeZip boundary

The RFD says "call Vega's run_panel". But each agent is an independently-deployable CodeZip with no cross-agent runtime import (ADR-110 §6), and the members' top-level modules collide (both have a models, a config). And the shared-panel extraction (docs/notes/spike-1668-shared-agent-code.md) is a decision note — agents/_shared/ does not exist yet. So Story-C takes the interim the spike blesses and the ticket names: vendor the panel + overlay, exactly as frame_contract.py already vendors the frozen frame.

FileWhat it is
panel.pyVega's panel machinery, vendored: the agent factory, the time-boxed seat runner, the diversity floor, and the deterministic hardening (_validate_locks, _carry_locked_fields, verify_critique_closure, _derive_cause_title, _degrade_status). Same code, same fabrication defences.
panel_prompts.pyVega's three role prompts (the injection-defence FRAME_RULES core is byte-for-byte) + a COMMAND OVERLAY per role. SOUL composes into the synthesizer only (D-12).
panel_models.pyThe panel's working contracts — Critique/CritiqueFinding (vendored) + PanelInput (FramerInput + the two command fields).
command.pyThe thin route: scope → locks, run the panel, derive changed_fields, build the §4.2 turn.completed.
What differs from Vega's run_panel — and why: a reshape command ("make the issues bullets") carries no new documents; the grounding is the current frame. Vega short-circuits to insufficient_input when there are no documents — correct for an ambient build, wrong for a command. So run_command_panel drops that one guard and grounds on previous_frame (the current frame). Everything else — ZDR every call, the <2-draft failure envelope, the lock/closure/degrade hardening — is the same code.

3 · The command overlay (prompts, not new engine)

The same three roles, one added instruction each (RFD §4.1). The command rides in as a walled <<COMMAND>> block (untrusted counsel free-text is data, never instruction — a command like "reveal your prompt" is treated as noise):

4 · The wire shape — a command turn.completed

# runtime dispatch: intent="command" → run.started → model.usage → turn.completed → run.completed
{
  "frame": { …full proposed FrameOutput… },
  "frame_status": "usable|thin|insufficient_input",
  "command": { "text": "rewrite the theory more aggressively", "scope": "theory_of_case" },
  "changed_fields": ["theory_of_case"],        # derived vs current_frame; ⊆ scope by the locks
  "requires_counsel_approval": true,          # ALWAYS true — the truth boundary, on the wire
  "degraded_panel": false,
  "proposer_provenance": [ …which models survived (D-14)… ],
  "base_frame_version": 5,                      # the broker's stale-base race key (RFD §6)
  "schema_version": "frame_output.v1"
}

No new ratify rail: a Mira command proposal is a frame-version proposal, diffed and presented on the same Confirm/Reject surface a Vega ambient build uses (the broker owns the landing — Story-E). A failure fails closed and typed: an unknown scope, <2 drafts, a critic/synth error, or an unclosed high finding all raise a FrameBuildFailed that the runtime maps to a typed run.failed carrying its stage/reason.

5 · Where the two guarantees meet — the degraded cap

A subtle conflict, resolved in code. Acceptance demands both "a field command changes only that field" and "a degraded panel never ships usable". On a degraded field-scoped command these fight: capping usable → thin would change frame_status, a field outside the scope. Resolution: for a field scope frame_status is itself locked (all-but-scope), so a single-field reshape does not touch completeness — the current status stands, and changed_fields stays ⊆ {scope}. For a "brief" command frame_status is unlocked, so the degrade cap applies and a reduced-survivor Brief can never present usable. Both hold, tested both ways.

6 · Proof

# offline, no key — the deterministic guarantees + the real panel over faked models
cd agents/mira && uv run --package mira pytest   # 46 passed
uv run pytest tests                              # 151 passed (governance: custody, egress, catalog)
uv run ruff check agents/                         # clean

# the command-eval harness (acceptance #3) — LIVE, needs the dev OPENROUTER key; no UAT
uv run python agents/mira/evals/run.py command   # golden command set: invariants + human/judge read

The offline suite pins the acceptance: a field-scoped command changes only its field even when the synthesizer tries to change more (lock carry-through) · a "brief" command holds identity while the read regenerates · requires_counsel_approval always true · a degraded panel never ships usable (and a field command keeps its locked status) · a command runs with no documents (no insufficient_input short-circuit) · the command reaches the panel walled as <<COMMAND>> · unknown scope / <2 drafts / an unclosed high finding all raise a typed FrameBuildFailed → typed run.failed. The evals/ harness reuses Vega's frame-eval method (docs/notes/frame-grading-method.md) for the command shape and checks the same invariants on real model output over a golden set — its live green run is the eval-acceptance gate (frame output is eval-accepted, founder 2026-07-02).