TL;DR: A2A, MCP, the registry, and the catalog all ask for a card about the same project. We answered A2A with a copy. The comment said one context, every door. The wire said raw .fafa provenance at the wrong URI. That's why faf cards exists.

In Plain English

Old state. A project speaks through several doors — MCP, A2A, a registry, a catalog. Each one asks “who are you?” If every door writes its own answer, those answers drift. Then the project is lying about itself. We used to do that. The comment said the A2A card carried the same context as MCP. The wire said otherwise.

Fix. One projector. You write the project once (.faf is what it is; .fafa is who the agent is). faf cards writes the host cards. Same context block on every door. It will not invent a door or an agent.

New state. That is faf-cli 7.8.0. Claude FAF 5.22.0 now uses that projector, as will all FAF apps. We do not replace MCP or A2A. We fill the slot they already have.

What we told ourselves

Claude FAF already mapped .fafa onto an A2A Agent Card. The file comment was clean: the FAF context block rides as an A2A extension, byte-equivalent to MCP _meta["one.faf/context"]. One context, every door.

That sentence is the product. It was not what left the function.

What the wire had

Two lines. That's the whole receipt.

We saidOn the wire
URIthe same context as MCPhttps://one.faf/context
paramsfafContextBlock()fafa.provenance

MCP already used the dereference key one.faf/context and the durable block. A2A used a lookalike URI and dumped whatever was in provenance — including unofficial fields like provenance.version. Same project. Two doors. Two truths.

The rule

A comment that says “byte-equivalent” is not a check. If two doors can disagree, the project is lying. FAF don't lie has to hold on the wire, not in the file header.

That's the README bug, on a protocol

We already measured this in Nineteen Hours. A fact copied into a second file is a fact with a half-life. Discipline doesn't stop it. Shipping another host makes it worse.

A card on the wire is just another file. A2A wanted a card. MCP wanted _meta. The registry wanted server.json. The catalog wanted a row. The instinct is to answer each one. That's four writers and a promise they will stay in sync.

They will not.

So we built one projector

Not four better mappers. One function that writes the host cards from .faf + .fafa.

  • Same params — fafContextBlock() on A2A extensions[].params, MCP _meta["one.faf/context"], and registry publisher-provided.
  • A2A URI is the dereference https://faf.one/context — never https://one.faf/context.
  • No invented door. Authored endpoints win. --door-url is allowed when you already have a door. No door, no flag: it refuses.

faf server-card still exists. It now wraps the same projector. Claude FAF 5.22 is the floor: fafaToA2ACard is a thin adapter. The door is the caller's URL. Unofficial provenance.version is dropped. Core is still 12 tools. Display title stays Claude FAF.

The adapter is a door. It does not get a second story.

WITH, not MORE

This is not a fifth card format. The format did not grow. A2A did not get replaced. MCP did not get replaced. Complementary — we fill the slot the host already has.

A projection is a view. .faf stays the source. If you change the file and the A2A params no longer match MCP _meta, the projector is broken. That is the test.

The two ships

The release notes are the versions. This is why they exist.

faf cards --check