The frame learns to name the case

matter.title is whatever counsel typed in the intake box — "Rahul Rathode vs Sugar Pipes", or "FIR no. 23/2025". A brief cannot open with that. It needs the cause title: the formal legal heading — party-vs-party with roles (Claimant v. Respondent), the forum, and the case number. Story-2017 adds that field to FrameOutput and has Vega derive it in code from facts she already settled — or leave it blank when the record can't form one.
Story-2017 · repo pramaan-agents · branch issue-2017-cause-title-frameoutput · pramaan-docs ADR-117 (the type is the contract) · ADR-153 (schema custody) · source-of-truth contract only — functions mirror + app src/gen are downstream follow-ons

This is a source-repo change: the field is born on FrameOutput, the one place ADR-117 lets a frame field exist, and the schema-custody artifacts under schemas/ are regenerated so the downstream pramaan-functions mirror and app src/gen can carry it through in their own PRs.

Why cause_title is derived in code, not asked of the model: the heading is a pure function of facts Vega already settled — the parties (with their procedural labels), the tribunal/forum, and the case reference. A model asked to "write the cause title" can invent a forum or a case number that isn't in the record; a deterministic composer cannot. So the code owns it and overrides whatever a model may have guessed for the field. That is the same honesty stance as every other optional legal field: a blank that validates is honest, an invented heading is a lie.

1 · The contract grows one field

# agents/framer/models.py — FrameOutput, Constitution tier
case_ref: str | None = None
cause_title: str | None = None   # FORMAL heading; DERIVED; None when insufficient

Nullable, defaults None — a thin matter leaves it blank rather than fabricating. Adding it changes FrameOutput.model_json_schema() and the field vocabulary, so schemas/frame_output.v1.schema.json and .fields.json were regenerated via agents/framer/schema.py (the ADR-153 custody gate fails CI otherwise).

2 · Vega derives it — in the deterministic hardening step

# agents/framer/agent.py — run_panel step 5, BEFORE lock carry-through
final = _derive_cause_title(final)   # deterministic; overrides any model guess
final = _carry_locked_fields(final, data)   # a locked/counsel-edited title wins
InputContributes
parties (side us/them)The litigants, each labelled by participant_label_name when present ("Acarer (Claimant)"). Side other (the tribunal) is not a party to the cause and is excluded.
tribunal_name / forum_typeThe forum the heading sits before (named tribunal preferred, forum type as fallback).
case_refAppended as the case number when present.

Result: Acarer (Claimant) v. Hira Ferro (Respondent) before Sole Arbitrator Hon'ble Jayant Nath (ARB 23/2025).

The honesty gate: _derive_cause_title returns cause_title=None unless the record names an opponent on each side (an us and a them) and a forum. One-sided parties, or no forum, can't form a party-vs-party heading — so it stays blank rather than half-invented. Ordering matters: it runs before _carry_locked_fields, so a heading counsel edited and locked on the previous frame is restored and wins over a fresh derivation.

3 · Round-trip: derive, gate, and survive a counsel edit

# agents/framer/tests/test_panel.py — 3 new tests
test_cause_title_is_derived_and_overrides_a_model_guess     # composed from identity facts; model garbage discarded
test_cause_title_stays_none_when_no_opponent_is_named       # honesty gate — blank, not invented
test_cause_title_survives_ingest_and_a_locked_counsel_edit  # edited + locked heading carries through a rebuild

The third is the round-trip the AC asks for: a frame ingested with a counsel-edited cause_title, locked, survives a full panel rebuild that would otherwise derive a different heading — the counsel edit is what ships. (The frame edit endpoint itself lives in pramaan-functions; the lock carry-through is its in-brain analog and the mechanism by which a counsel edit persists across a reframe.)

4 · Out of scope (downstream PRs)

The pramaan-functions FrameOutput mirror + OpenAPI carry-through, and the pramaan-app src/gen regeneration (Zod + types so the app reads frame.cause_title), are separate follow-on PRs in their own repos. This ticket is the source-of-truth contract + Vega derivation only — the custody artifacts here are what those repos generate against.

5 · Proof

# offline, no key
uv run --package framer pytest agents/framer/tests   # 100 passed (incl. 3 new)
uv run python agents/framer/schema.py                # custody artifacts regenerated
uv run ruff check agents/framer                       # clean