---
mcp-manifest: auto
name: voice-mirror
description: "Authorial-voice capture and application. Captures the operator's voice from their own writing (the `human-only` corpus) and conditions agent drafts on it. Five tools: tag historical content (operator supplies `format`), distil a per-format style card, retrieve conditioning for a drafting brief (requires `brief.format`), record operator edits as feedback, and capture the current session's turns on demand. Other drafting skills (email composition, property-brochure, prospectus) call it transparently — opt out via `voiceMirror: false` in the calling skill's frontmatter. A core plugin: present on every brand, no premium bundle required."
icon: 🪞
tools:
  - name: voice-tag-content
    publicAllowlist: false
    adminAllowlist: true
    riskClass: write_local
  - name: voice-distil-profile
    publicAllowlist: false
    adminAllowlist: true
    riskClass: write_local
  - name: voice-retrieve-conditioning
    publicAllowlist: false
    adminAllowlist: false
    riskClass: read
  - name: voice-record-feedback
    publicAllowlist: false
    adminAllowlist: true
    riskClass: write_local
  - name: voice-ingest-session-text
    publicAllowlist: false
    adminAllowlist: true
    riskClass: write_local
mcp:
  command: node
  args:
    - ${PLATFORM_ROOT}/lib/mcp-spawn-tee/dist/index.js
    - ${PLATFORM_ROOT}/plugins/voice-mirror/mcp/dist/index.js
  env:
    MCP_SPAWN_TEE_NAME: voice-mirror
    LOG_DIR: ${LOG_DIR}
    PLATFORM_ROOT: ${PLATFORM_ROOT}
    ACCOUNT_ID: ${ACCOUNT_ID}
    SESSION_ID: ${SESSION_ID}
---

# Voice Mirror

Capture the operator's authorial voice from their own writing (the `human-only` corpus) and condition agent drafts on it. A core plugin — present on every brand.

The `voice-mirror` skill drives the operator-facing flow (backfill tagging, distillation, session capture). Other drafting skills call `voice-retrieve-conditioning` transparently before composing, so prose that goes out under the operator's name is conditioned on their style card, not a neutral register.

## When to Activate

The operator is:
- Building or refreshing their writing-voice profile from their own historical content
- Asking to capture the current session's voice
- Drafting anything that goes out under their name (email, social post, article, marketing copy, note) — the drafting skill retrieves conditioning automatically

## Skill

| Skill | Purpose |
|-------|---------|
| `voice-mirror` | Capture the operator's voice from their own writing (`human-only` corpus) and condition agent drafts on it. Five tools: tag historical content (operator supplies `format`), distil a per-format style card, retrieve conditioning for a drafting brief (requires `brief.format`), record operator edits as feedback, and capture the current session's turns on demand. Other drafting skills (email composition, property-brochure, prospectus) call it transparently — opt out via `voiceMirror: false` in the calling skill's frontmatter. |

## Tools

Voice mirror introduces five deterministic plugin tools. They write to the graph directly over an account-scoped Cypher session, not through `memory-write`: an authorship stamp does not need the re-embedding and re-validation `memory-write` performs on every call. Account scoping is enforced per query, and identity per write (see [Identity](#identity)).

| Tool | Purpose |
|------|---------|
| `voice-tag-content` | Stamp `authorshipMode ∈ {human-only, human-led-agent-assisted, agent-led-human-reviewed, agent-only, unknown}`, `format ∈ {formal, informal, text, email, social-post, article, note, marketing-copy}` (`formal`/`informal` are the broad buckets and the default; the rest are narrow), and `voiceAuthor` on one or many `:KnowledgeDocument | :Message | :SocialPost` nodes (email threads live as `:KnowledgeDocument {source:'email'}`). Bulk-mode for backfill batches. `format` is required. `author` is optional — omit to attribute to the tagging operator. The resolved `voiceAuthor` must name an `:AdminUser` on the calling account or the tag is refused and nothing is written (see [Identity](#identity)). |
| `voice-distil-profile` | Three modes, scope-aware (`scope='personal'` default walks one author; `scope='org'` walks the whole account onto the business). `mode='sample'` (default) walks the `human-only` corpus for the given `format`/`scope` and returns exemplars + recent edit intents for the agent to compose a style card; cadence-guarded (≥20% growth or ≥30 days). `mode='amend'` reads only `nodeIds` plus the existing `:VoiceProfile.styleCard` so the agent can decide whether named documents move the profile — operator-initiated, no cadence guard. `mode='write'` persists the YAML card; supply `amendedFromNodeIds` to attribute the write to specific documents (bypasses cadence + corpus walk). Trashed nodes are excluded from every walk. Omit `format` on `'sample'` to enumerate all formats in that scope's corpus and distil each. |
| `voice-retrieve-conditioning` | Return `{styleCard, exemplars[], status}` for a drafting brief. Requires `brief.format` (one of the six corpus formats). `brief.scope` (default `personal`) picks personal vs org voice; a personal request with no personal profile falls back to the org profile (`status='fallback-org'`). K=5 short-form (`brief.length:'short'`), K=15 long-form (`brief.length:'long'`). Token-budget bounded. `brief.topic` is optional and **ranks** the exemplar walk — its terms order the corpus by how many they hit, recency breaks the tie, and a topic that matches nothing still returns the K most recent. `status ∈ {ok, fallback-org, no-data, error}`; `ok` means the query ran, not that exemplars came back — read `exemplars.length` for that. Identity is not gated here: retrieval writes nothing, and a personal request that resolves no personal voice falls back to the org voice by design. |
| `voice-record-feedback` | Capture an operator edit on an agent draft as a `:VoiceEdit {format, scope}` with a Haiku-summarised `intent`, linked back to the scoped per-format `:VoiceProfile` via `:FEEDBACK_FOR` (org edits → org profile; editor preserved via `:AUTHORED`). Requires `format`. |
| `voice-ingest-session-text` | Write operator turns from the current session as `:Message {format:'text', authorshipMode:'human-only'}` corpus nodes. The agent reads its own operator turns from context and passes them as `turns`. Deduplicates via SHA256 `contentHash`. Invoked on demand by the voice-mirror skill when the operator asks to capture this session's voice. |

## Identity

Every `userId` / `author` a tool accepts is an `:AdminUser` id that already exists on the calling account. It is never a person's name, nor a slug derived from one. A value naming no `:AdminUser` on the account is refused and nothing is written: a profile keyed on a non-existent admin is unreachable by any seat that resolves identity properly, and a `voiceAuthor` stamped with one attributes a corpus to an identity that does not exist.

The reserved org key `__org__` is exempt. It names no `:AdminUser` by design and anchors on `:LocalBusiness`. That is how the account-wide house voice works, including on a spawn that carries `ACCOUNT_ID` but no personal identity.

Each tool resolves its identity from `USER_ID` in the spawn env. A tool argument is only needed when attributing to someone other than the caller, on a multi-operator account.

A refusal names the rejected key and the account, and emits one line:

```
[voice-identity] refuse tool=<tool> reason=unknown-admin-user userId=<key> accountId=<account>
```

`platform/scripts/voice-author-key-audit.sh` is the standing per-install check for keys already in the graph. It is read-only and reports two distinct findings: `:VoiceProfile` keys and `voiceAuthor` stamps matching no `:AdminUser` on their account. Disposition of anything it finds is an operator decision.

## References

No external references; the skill body defines the five tool contracts. Developer notes in `maxy-code/.docs/voice-mirror.md`.
