Story-3272 Walkthrough

Outcome: the agents an Indian lawyer talks to are now named Kabir, Zoya, Neel and Vani. Nothing an engineer looks at moved: Logfire, agent keys, directories, envelope contracts and identity.py still say Blake, Vega, Indigo, Vani. That is not an oversight — it is the whole design.

The one idea: two namespaces that disagree on purpose

An agent has a permanent ROLE and a permanent code identity. Its PERSONA — the first name a lawyer sees or hears — is a regional presentation skin. India is the only live region, so there is exactly one skin.

NamespaceSpoken inThe cast
Engineering (global) Logfire service_name + gen_ai.agent.name, identity.py display_name, <agent>.yaml identity.name, eval-suite names, dashboards, docs, PRs, agent keys, directory names Blake · Vega · Indigo · Vani
Counsel-visible (regional) system prompts, SOUL.md, model-facing tool descriptions, product copy Kabir (registrar) · Zoya (framer) · Neel (indigo) · Vani (mira)
Do not "align" them. A PR that makes display_name agree with the map has broken one of the two namespaces — every saved Logfire query and every eval-kit lookup keys on the global name. The map itself carries this warning in its module docstring so nobody has to rediscover it.

Where the names live now

agents/_shared/personas.py          ← the ONLY place a persona first name is written
  COUNSEL_FACING_PERSONAS = {
      "mira": "Vani", "registrar": "Kabir", "framer": "Zoya", "indigo": "Neel",
  }
  persona(agent_key) -> str           ← called at IMPORT time, never per run

  agents/<agent>/SOUL.md              ← carries a {persona} slot, filled at load
  agents/<agent>/prompts/*.py         ← f-string {PERSONA}, or .replace() where the
                                        prompt contains literal JSON braces

Keyed by machine agent key (the directory name / identity.key), never by persona — so the key that resolves a name is the one thing that never changes.

What is deliberately absent

Prompt caching: one byte change, fleet-wide, accepted

Every persona name is resolved once at import and interpolated into a module-level constant, so the stable prompt prefix is still byte-identical run to run (AGENTS.md CACHING-FIRST PROMPTS). What did change is the prefix itself, once, for every agent — a cold cache on the next deploy. Founder ruling: expected, note it, do nothing about it.

Re-enrichment: bumped, not suppressed

A rename is a prompt change, so the prompt versions bumped normally and staging re-enriches — free evidence that the renamed prompts work, with zero clients on the line.

PromptVersion
Zoya proposer / critic / synthesizer / triagev3 / v3 / v3 / v6
Kabir card (card_engine lineage)registrar-card-v3 → v4
Kabir CARD_SYSTEM (agent.py lineage)registrar-card-v2 → v5 *
Kabir carveregistrar-carve-v1 → v2
Kabir segmentregistrar-segment-v2 → v3
Kabir family proposer / judge…-v2 → v3 (both)
Vani conversemira.converse.v17 → v18
Neel (all four passes)auto — Indigo mints its version from the composed text

* _CARD_PROMPT_VERSION skips v3 and v4 because two different prompts share the registrar-card-vN string namespace (pre-existing); stepping past them keeps each version meaning one thing.

Not bumped, and why: registrar-classify-v1 and registrar-label-v1 are Pydantic Literal pins on four emitted artifact contracts (classify_models, govern_models, batch_models ×2, revert_models). This repo has never bumped them on a prompt edit — the classify prompt changed in #158 without one — so bumping now would be a wire-contract change, not the normal bump the ruling asked for. Flagged rather than done silently.

The one behaviour change counsel will notice

counsel: "tighten the issues section"
  → Vani calls request_frame_update
  → runtime answers exactly:  I've asked Zoya.      (was: I've asked Vega.)

Vani herself is unchanged. What changed is the name of the colleague she delegates to. FRAME_UPDATE_ACKNOWLEDGEMENT is now derived from the map, and the tests that pinned the old literal assert the constant instead — so the string can never drift from the prompt that teaches it.

A silent-drop this change found and fixed

Three registrar prompts carry literal JSON braces, so they cannot be f-strings and use "...".replace("{persona}", PERSONA). That turns the assignment into an ast.Call, and the lab's prompt inventory only recognised ast.Constant and ast.JoinedStr — so carve.py dropped out of the prompt lens entirely, silently. The governance test caught it. Fixed at the root in evalkit/lab.py: the parser now unwraps a .replace() chain, so the lens renders the filled prompt.

What the corpus kept

Adversarial .jsonl and dataset inputs still say Blake / Vega — the corpus is a ratchet, and an attack string is evidence of what was tried, not product copy. No golden needed regenerating: no counsel-facing generated-output assertion carried a persona name. Zero .jsonl files changed.

Verification

rg -n "You are (Blake|Vega|Indigo)" agents/     → 0 hits

ruff check agents/ evalkit/ tests/ scripts/    → All checks passed

pytest tests/                    457 passed
pytest agents/framer/tests       164 passed
pytest agents/indigo/tests       326 passed
pytest agents/registrar/tests    521 passed
pytest agents/mira/tests         395 passed
pytest agents/context_judge/tests 58 passed
pytest agents/atlas/tests        516 passed

Re-measured, not relaxed: the mira tool-wire token deltas moved 2505→2508 and 2278→2279, and the omitted-channel prompt hash was re-pinned. "Vega" and "Zoya" are both four characters but do not tokenize alike — the byte length of the prompt is unchanged at 20,990, only its bytes are.