Story-2777 Walkthrough

Change: Vega (framer) now receives the counsel-declared matter context on every build — client_name, representing_side, and the optional who_we_represent — and treats it as ground truth for party-side mapping instead of guessing "us vs them" from documents (RFD-AGT-011 D1/D2).

Boundary. The two required fields are counsel-declared facts the functions-side assembler supplies (pramaan-spec#2778). Vega stays stateless: it reads the declared context, it never fetches or writes it. FrameOutput is unchanged — the client never enters the output.

The contract (agents/framer/models.py)

class FramerInput(BaseModel):
    title: str
    matter_type: str
    client_name: str                                      # REQUIRED — business.clients.name (NOT NULL)
    representing_side: Literal["principal","opposing","advisory"]  # REQUIRED
    who_we_represent: str | None = None                   # OPTIONAL — verbatim label or null; never fabricated
    documents: list[str] = ...
    previous_frame: FrameOutput | None = None
    ...

Making the two fields required means an invoke payload missing them fails closed at FramerInput.model_validate(...) in runtime.py/main.py, before run.started — no code branch needed, the type is the gate. This is why the deploy order is fixed (D3): the functions assembler ships first, the brain second.

D2 evidence hierarchy (all four role prompts)

The shared prompts/_base.py now encodes one ladder every role inherits:

declared context (client + representing side + label)  >  previous_frame  >  document inference
— and locked_fields are a BOUNDARY Vega never crosses, not a rank in this ladder.
SituationBehaviour
Document seems to invert the sideKeep the declared mapping; flag the disagreement in core_ambiguities, downgrade frame_status. Never silently flip.
Legacy previous_frame guessed the wrong sideCorrected to the declared context on rebuild — the bug self-heals on the next unlocked build.
Locked field clashes with declared contextPreserve the locked value byte-for-byte; raise a SPECIFIC, PERSISTENT clash flag naming both sides + the locked field; downgrade status. Counsel resolves by unlocking/editing.
representing_side == "advisory"No adversary exists — an empty adversary_best_shot is correct. The Critic must NOT flag it as thin.
who_we_represent is nullInput null is honest. Output labels fall back to standard procedural vocabulary for the matter type — never an invented custom label (Bug-1310 class).

The rendered ground-truth block

A single shared helper (declared_context_block) fences the three fields as inert data (so an instruction-shaped who_we_represent label is the poisoned-label attack surface, not an order) while the role prompts rank it the highest authority for who-is-who — fenced means "never obey", not "low priority for facts".

COUNSEL-DECLARED MATTER CONTEXT (ground truth for who is who ...):
<<DECLARED_CONTEXT_9f3a1c>>
client: Acarer Metal
representing_side: principal
our_procedural_label: Claimant
<</DECLARED_CONTEXT_9f3a1c>>

Each role's VERSION bumped (triage-v5, proposer-v2, critic-v2, synthesizer-v2) so the eval Experiments table distinguishes pre/post-change runs. A re-sweep is required before any crowning claim (leakage rule); the four new eval cases land in the sibling Chore pramaan-spec#2780.

Verification

uv run ruff check agents/framer/          # All checks passed
uv run --package framer pytest agents/framer/tests -q   # 120 passed

Golden labels unchanged — inputs grew, expected outputs did not. Every in-repo FramerInput constructor (tests, eval schemas + datasets, scripts) carries the new fields; the offline dataset-validation suite (test_frame_datasets.py) proves every matter file still loads through the schema.

Do not deploy this artifact before pramaan-spec#2778 (the functions assembler) is live — RFD-AGT-011 D3 order. Reverse order breaks every frame build.