Agent-First Mail v0.3: The Workspace Knows Who
v0.3 gives the mailbox workspace a sense of who: contact cards that auto-link to cases and materialise onto message views, and an identity registry of named send personas your drafts can write footers from — both still behind the one push boundary.
afmail v0.1 drew the line — your agent reads and works in local files, and the real mailbox only changes on --confirm. v0.2 collapsed every queued effect into one all-or-nothing push. v0.3 keeps both and fills in something the workspace was missing: a durable sense of who — who you’re talking to, and who you’re sending as.
Until now a message was evidence and nothing more. The sender was a header string, re-parsed every time, with no place to attach “this is a known buyer” or “we’ve dealt with them before.” And every draft went out as the same anonymous you. v0.3 makes both of those first-class, file-first, and — like everything in afmail — local until you push.
Contact cards: who you’re talking to
A contact is a card in the workspace, with its own stable UID (pYYYYMMDDNNN), organised into groups, mirroring the case lifecycle you already know:
afmail contact create --name "Zhang San" --email zhang@example.com --org Acme --role Buyer
afmail contact list --group people
afmail contact email add p20260620001 zhang-alt@example.com
afmail contact tag p20260620001 vip
afmail contact notes append p20260620001 --body "Prefers Chinese; replies within a day."
afmail contact archive p20260620001 --reason "left the company"
Email addresses are globally unique across all contacts — one address belongs to exactly one card — so the workspace can keep a clean email→contact index and never guess between two people. You don’t have to enter everyone by hand, either; afmail can stub cards straight from the senders already sitting in your mail:
afmail contact extract --from-triage # or --from-case, or --all
The payoff shows up where you do the work. When you open a case, afmail auto-links the sender’s contact_uid if their address is in the index — so a case carries who it’s with, not just a raw From line. And the contact link is materialised onto the message view itself, refreshed automatically whenever a contact’s emails or name change:
afmail case create --name "Order delay" --message message_inbox_88213_4 --reason "buyer following up"
afmail message show message_inbox_88213_4 # the rendered view now shows the linked contact
Reading stays exactly what it’s always been in afmail — open the Markdown. There’s just more in it now, and it stays correct on its own. If you add an alias or rename a card later, afmail contact rebuild re-derives the index and re-links existing cases in one pass.
Identities: who you’re sending as
The other half of who is outbound. A workspace can now define named send personas in config, each with an optional footer note at identities/<slug>.md:
afmail case draft reply c20260620001 message_inbox_88213_4 --identity support
afmail case draft change c20260620001 reply-message_inbox_88213_4.md --identity sales
--identity SLUG works on case draft new, case draft reply, and case draft change; omit it and afmail uses the configured default. The crucial part is that the identity footer is written into the draft body, not appended invisibly at send time. It’s there in the file, in the draft show preview, for you to read before anything leaves the machine:
afmail case draft show c20260620001 reply-message_inbox_88213_4.md # footer included, exactly as it will send
No hidden send-time additions. What you review is what goes out — and it still only goes out on the one push.
Same boundary, fewer ways to leave a mess
v0.3 also tightens the workspace itself, so the new state never leaks or strands:
- Atomic create. If any step of creating a case or archive entry fails, afmail rolls back the directory it just made — no orphaned
cases/open/...left behind to confuse the next command. - Reliable recovery. Interrupted-transaction sentinels are now preserved correctly, so a retried command detects a previously-failed transaction and recovers instead of building on a half-applied state.
- Validate before you write.
archive message createchecks the related-message constraint up front, before creating any files — and the skill now spells out that messages withrelated_message_idsbelong grouped into a case. - Cleaner skill install. Installing skills adds each skill directory to the workspace
.gitignoremanaged block, covering both.codex/and.claude/agent paths without duplication.
Install
brew install agentfirstkit/tap/afmail # macOS / Linux
scoop bucket add agentfirstkit https://github.com/agentfirstkit/scoop-bucket
scoop install afmail # Windows
cargo install agent-first-mail # any platform
After installing, run afmail skill install so your agent reloads afmail’s behavior rules.