---
name: email-composition
description: "Conversational wrapper around the email plugin tools. Activate when the operator asks to reply to an email, draft a new email, follow up on a thread, or triage outstanding inbox items (e.g. 'reply to Sarah about X', 'email Joel about Y', 'what's outstanding in my inbox'). Orchestrates `email-graph-query`, `email-search`, `email-read`, `email-send`, `email-reply`. Every send is operator-approved — never auto-sends."
---

# Email composition

Conversational wrapper around the existing `email-*` tools. Drafting, threading, triage. **Human-in-the-loop only — every send is explicitly approved by the operator.** No drip sequences, no scheduled sends, no automatic categorisation.

## When to activate

Activate when the operator asks for any of:

- **Reply to a known thread.** "Reply to Sarah's email about the lease." "Draft a reply to that thread from Joel."
- **New outbound email.** "Email Joel about the viewing." "Send a note to the team about Friday."
- **Follow-up on a topic.** "Follow up with everyone from yesterday's viewings."
- **Inbox triage.** "What's outstanding in my inbox?" "Anything I need to deal with?"

Do **not** activate for:

- Listing or searching emails as the end-goal (no draft intent) — let `email-search` / `email-graph-query` answer directly.
- Setup, status checks, OTP extraction — those are direct tool calls.
- Any automated send loop. If the operator asks for drip / scheduled / broadcast sends, decline and explain that this skill is human-in-the-loop only.

## Flow — reply to an existing thread

### 1. Identify the thread

Pick the right retrieval path:

- **Graph history first** (`email-graph-query`) — for "reply to Sarah's email about the lease": the operator is naming a known correspondent and topic. Fast, semantic, covers the operator's full email history.
- **Live IMAP** (`email-search` or `email-read`) — for "that email that just came in" or "today's reply from Joel": the operator is naming a recent message that may not yet be in the graph.

If the search returns **multiple plausible threads**, do not guess. Surface 3-5 candidates with sender, subject, date, and a one-line summary, and ask which one. Emit narration `[email-composition] ambiguous-thread candidates=<n>`.

If the search returns **zero matches**, ask the operator to refine (different correspondent? different subject phrasing? wider date range?). Do not invent a thread.

Once the thread is locked, emit narration `[email-composition] thread-identified messageId=<id> subject="<subject>"`.

### 2. Load thread context

Pull the full thread — the current message plus parent messages — via `email-read` or `email-graph-query`. Reply to the actual position of the conversation, not a stripped-down summary. Quote-stripped one-line summaries miss the question the operator is actually being asked.

If the thread has **attachments** that the draft will reference, list the attachment names in the draft presentation and ask whether to forward them. The five existing tools do not currently forward attachments inline — surface this so the operator can manually re-attach if needed.

### 3. Detect skip conditions

Before drafting, check the inbound for:

- **Out-of-office / vacation autoreply.** Skip. Flag to the operator: "this looks like an OOO autoreply — no draft." Do not propose a reply.
- **Unsubscribe / list-management mail** (List-Unsubscribe header, "to unsubscribe click here" body). Skip. Flag to the operator.
- **Hostile or grief tone.** Draft cautiously and flag the tone in the presentation: "this reads as hostile / grief-related — please review carefully before sending." Never auto-soften without surfacing the tone judgement.

### 4. Retrieve voice exemplars (conditional)

Call `voice-retrieve-conditioning` with `format: "email"`, `length: "short"`, and `topic` set to the inbound thread's subject (or, for a new outbound, the topic the operator named). The tool returns `{styleCard, exemplars[]}` — when `styleCard` is non-null, inject both the style card and the exemplar bodies into the drafting prompt before composing.

If the tool returns `{styleCard: null, exemplars: []}` (the operator has no `:VoiceProfile` yet, or the `writer-craft` plugin isn't enabled on the account), draft in a **neutral British business register** — polite, concise, no slang, no transatlantic spellings — and state explicitly in the presentation: "drafted in neutral British business voice (no voice profile on file)."

Emit narration `[email-composition] voice-mirror styleCardBytes=<n> exemplars=<k>` when conditioning was applied. Absence of the line confirms the fallback path was taken.

### 5. Draft

Compose the reply. Anchor to:

- The specific question or position in the parent message.
- The operator's voice (from voice-mirror) or the neutral British business register.
- Any decisions the operator has already stated in this conversation (e.g. "say yes to the price, no to the timeline").

Keep drafts tight. Email is not a wall-of-text medium.

### 6. Present

Show the draft inline with:

- **To** — recipient address (the original sender, unless the operator asked for `replyAll`).
- **Cc / Bcc** — empty unless the operator explicitly named one. Never copy people forward from the original thread without being asked.
- **Subject** — the threaded subject (the original with `Re:` if not already prefixed; `email-reply` handles this).
- **Body** — the draft.
- **Notes** — any flags from step 3 (tone, attachments, OOO skip-already-applied).

Emit narration `[email-composition] draft-presented recipient=<addr> subjectPrefix="<first-60-chars>"`.

**Do not call `email-reply` at this step.** The send is not yet approved.

### 7. Edit loop

The operator may:

- **Approve** ("send it", "looks good", "yes"). Proceed to step 8.
- **Edit verbally** ("make it shorter", "drop the second paragraph", "say yes to the price but not the timeline"). Apply the edit and re-present. Stay in the loop until approval or abandonment.
- **Abandon** ("never mind", "I'll do it myself"). Emit `[email-composition] send-aborted reason=operator-abandoned`. Do not send.

Every operator edit on the draft (before final approval) is downstream feedback for `voice-mirror`. After the operator approves the final send, call `voice-record-feedback` with the original agent draft and the final edited version (`skill: "email-composition"`, `brief` set to the topic/subject). The call is fire-and-forget — failure is logged but does not block the send. If the `writer-craft` plugin isn't enabled on the account the tool call is omitted; no feedback is recorded.

### 8. Send

On **explicit** approval, call `email-reply` with:

- `messageId` — the parent message ID from step 1. This is the threading anchor; `email-reply` populates `In-Reply-To` and `References` from it so the reply lands inside the existing thread, not as a new conversation.
- `replyAll` — only if the operator asked for it. Default is false (reply to sender only).
- Subject and body from the approved draft.

Emit narration `[email-composition] send-confirmed`.

Confirm to the operator that the send happened by quoting the `email-reply` tool's success line (it includes the sent `messageId` and the resolved `In-Reply-To`).

If `email-reply` errors, surface the error verbatim; do not silently retry. The operator decides whether to fix and retry or abandon.

## Flow — new outbound email

Same as the reply flow with two differences:

1. **No thread context load** (step 2 skipped). Instead, ask the operator for the recipient (if not already named), the topic, and any constraints (formal vs casual, length, things to include or exclude).
2. **Send via `email-send`**, not `email-reply`. There is no parent `messageId` because this is a new conversation.

Multi-recipient drafting: if the operator names a single person, use that address. If the operator names a group ("email the team", "everyone from yesterday's viewings"), ask which addresses to include — list candidates if you can resolve them from the graph, otherwise ask the operator to enumerate. Do not invent recipients.

## Flow — triage variant

When the operator asks "what's outstanding in my inbox" / "anything I need to deal with" / "what's unread":

1. Call `email-read` for the last N unread messages (default N = 10; operator can specify "last 25", "today's", etc.).
2. For each message, return a one-line summary with a recommended action:
   - **Reply** — needs a response.
   - **File** — informational, can be archived.
   - **Ignore** — autoreply, newsletter, list mail, OOO.
3. Emit narration `[email-composition] triage-returned n=<count>`.
4. Wait for the operator to pick one. When they do ("reply to #3", "draft a response to the Joel one"), drop into the reply flow at step 1 (thread already identified — use the `messageId` from the triage list).

The triage variant **never auto-drafts**. It returns the list and waits.

## Edge cases

| Situation | Behaviour |
|-----------|-----------|
| Multiple threads match the operator's description | Surface 3-5 candidates and ask. Never guess. |
| CC / BCC in the original thread | Reply to sender only by default. Ask explicitly before copying others. |
| `replyAll` requested | Pass `replyAll: true` to `email-reply`. Otherwise omit (defaults false). |
| Original thread has attachments referenced in the draft | List attachment names; ask whether to forward. |
| Inbound is an out-of-office autoreply | Skip. Flag to operator. Do not draft. |
| Inbound is a list / unsubscribe mail | Skip. Flag to operator. Do not draft. |
| Inbound is hostile or grief-related | Draft cautiously; flag tone in the presentation. Operator must review explicitly. |
| `voice-mirror` plugin absent | Draft in neutral British business register; state the absence in the presentation. |
| `email-reply` returns an error after approval | Surface the error verbatim. Operator decides whether to fix and retry or abandon. Do not silent-retry. |

## Out of scope

Hard boundaries — never offer these in this skill, even if asked:

- **Drip sequences** — multi-step automated send chains. Decline; refer to a future task.
- **Scheduled / delayed sends** — there is no "send at 9am tomorrow" path. The operator triggers the send when they approve.
- **List broadcasts** — bulk sends to managed subscriber lists. Not this skill.
- **Inbox auto-categorisation / labelling** — triage is on demand only.
- **Calendar follow-up auto-writes** — if a reply confirms a meeting, mention it and let the operator decide whether to write a calendar entry separately.
- **New MCP tools** — this skill orchestrates the existing five (`email-graph-query`, `email-search`, `email-read`, `email-send`, `email-reply`). Nothing else.

## Observability

The skill emits narration at each major step so a session log makes the full draft → approval → send chain greppable:

| Step | Narration |
|------|-----------|
| Thread locked | `[email-composition] thread-identified messageId=<id> subject="<subject>"` |
| Multiple matches | `[email-composition] ambiguous-thread candidates=<n>` |
| Draft shown | `[email-composition] draft-presented recipient=<addr> subjectPrefix="<first-60-chars>"` |
| Send approved | `[email-composition] send-confirmed` |
| Send abandoned | `[email-composition] send-aborted reason=<why>` |
| Triage list returned | `[email-composition] triage-returned n=<count>` |

The underlying `[email-send]` / `[email-reply]` / `[email-search]` / `[email-graph-query]` / `[email-read]` tool logs confirm the actual tool calls. Absence of an expected tool tag after `draft-presented` + operator approval is the failure signal — the skill said it sent but didn't.

Diagnostic on Pi:

```
grep -E '\[email-(composition|reply|send|search|graph-query|read)\]' <session-log>
```

returns the full chain for any thread.

## Dependencies

- **Required:** `email` plugin (`email-graph-query`, `email-search`, `email-read`, `email-send`, `email-reply`).
- **Optional:** `voice-mirror` plugin. If installed, drafts use the operator's style-card and edits are captured as feedback. If absent, drafts fall back to a neutral British business register.
