# voice (pi package)

A pi package that helps you build a corpus of your own writing, distill it into a style guide, and draft new text in your voice. Port of the `my-voice` Claude Code plugin, same directories, same workflow.

The premise: in-context grounding (rules + style guide + a few register-matched samples) outperforms generic LLM output for personal voice imitation, and beats most fine-tuning approaches at the corpus sizes individuals actually have. This package is the workflow.

## Install

```
pi install npm:@guygrigsby/pi-voice
```

Or from a local checkout:

```
pi install /path/to/pi-extensions/voice -l
```

The package registers its skills as `/skill:*` commands. Check with `pi list` and `/skill:voice-init`.

## Quick start

```
/skill:voice-init
```

That creates `~/.claude/voice/` (override with `$VOICE_HOME`) with starter templates and a corpus directory tree. Then:

1. **Edit `~/.claude/voice/rules.md`** with your hard writing rules. The starter file lists the most common AI-tells (em dashes, leading "I"/"I'm", redundancy). Add your own. The more specific these are, the better drafts will land.
2. **Populate the corpus.** Pick the sources you have:
   - `/skill:voice-pull-emails` — sent mail via a Gmail MCP server
   - `/skill:voice-pull-blog <url>` — your blog or any URL with prose
   - `bash ~/.claude/voice/scripts/pull_pr_descriptions.sh <github_username>` — your GitHub PR descriptions
   - `bash ~/.claude/voice/scripts/pull_review_comments.sh <github_username>` — your PR review and conversation comments
3. **Distill.** `/skill:voice-distill` reads the corpus and (re)writes `voice.md`.
4. **Draft.** `/skill:voice <register> <topic>` produces text in your voice.

## Skills

| Skill                        | What it does                                                       |
| ---------------------------- | ----------------------------------------------------------------- |
| `/skill:voice`               | Draft text in your voice. Pass a register and a topic.            |
| `/skill:voice-init`          | Create the corpus directory tree and copy templates.             |
| `/skill:voice-distill`       | Read the corpus and (re)generate `voice.md`. Preserves `rules.md`.|
| `/skill:voice-pull-emails`   | Pull recent sent emails via a Gmail MCP server.                  |
| `/skill:voice-pull-blog`     | Pull blog posts from a URL via web fetch.                        |

Shell helpers (in `$VOICE_HOME/scripts/` after `/skill:voice-init`):

| Script                       | What it does                                                                                     |
| ---------------------------- | ------------------------------------------------------------------------------------------------ |
| `pull_pr_descriptions.sh`    | Pull GitHub PR descriptions you authored. Splits into `pure` and `assisted` (AI-footer) buckets. |
| `pull_review_comments.sh`    | Pull GitHub PR review and conversation comments you authored.                                    |

## Drafting examples

```
/skill:voice email follow-up to a recruiter who hasn't replied in 2 weeks
/skill:voice pr-comment pushing back on a 1500-line generic helper that should be 4 small functions
/skill:voice review-summary close out a PR that was technically fine but scope-crept badly
/skill:voice slack ask the platform team for a CI pool with 32GB nodes
```

If you don't pass a register, the skill infers one from the topic and tells you what it picked.

## How it works

```
                     ┌─────────────────┐
                     │   rules.md      │   ← you write this. authoritative.
                     │ (hard rules)    │
                     └────────┬────────┘
                              │
        ┌─────────────────────┼─────────────────────┐
        │                     │                     │
        ▼                     ▼                     ▼
 corpus/emails/      corpus/github_prs/      corpus/blog/
 corpus/slack/       corpus/...
        │                     │                     │
        └─────────────────────┼─────────────────────┘
                              │
                     /skill:voice-distill
                              │
                              ▼
                       voice.md (style guide)
                              │
                              ▼
                        /skill:voice
                       (draft anything)
```

- `rules.md` is policy. You hand-edit it. The package treats it as authoritative and applies every rule to every draft.
- `corpus/` is data. The package reads from it for grounding (cadence, phrase choice, structural habits).
- `voice.md` is distilled summary. Generated by `/skill:voice-distill`. Refresh whenever the corpus grows materially.
- `/skill:voice` loads all three when drafting and pulls 3-5 register-matched samples from the corpus.

## Corpus filtering

Voice modeling fails when the corpus is dominated by low-effort or AI-mediated text. The package applies these filters by default:

- Drop bodies under 30 characters unless clearly content
- Drop one-line acks ("LGTM", "ok", "thanks", "👍")
- Drop content with agent-footer signatures ("Generated with Claude Code", etc.)
- Drop forwards with no original commentary
- Drop self-notes (body is just a URL or a token)

**Recent agentic-development output is not your pure voice.** Even without an explicit footer, PR descriptions written collaboratively with an LLM tend to have heavy structure (`## Summary`, `## Test plan`, acceptance-criteria tables, exhaustive checklists) and a different cadence. If you've been using AI in your dev loop, your last six months of personal-repo PRs are agent-mediated, not pure-you. Add those repos to the `exclude_repo_regex` arg of the GitHub scripts:

```
bash $VOICE_HOME/scripts/pull_pr_descriptions.sh alice 'alice/(my-side-project|prototype-x)'
```

Or after pulling, manually filter `pr_descriptions.jsonl` by `repo` and date.

## Where things live

```
$VOICE_HOME/                              (default: ~/.claude/voice/)
├── rules.md                              (you edit; authoritative)
├── voice.md                              (generated by /skill:voice-distill)
├── manifest.md                           (what's been ingested, when)
├── .gitignore                            (corpus/ ignored by default)
├── corpus/
│   ├── emails/<YYYY-MM>.jsonl
│   ├── blog/all_posts.md
│   ├── github_prs/
│   │   ├── pr_descriptions.jsonl
│   │   ├── pr_descriptions_pure.jsonl
│   │   ├── pr_descriptions_assisted.jsonl
│   │   └── review_comments.jsonl
│   └── slack/<workspace>.jsonl           (manual export)
└── scripts/
    ├── pull_pr_descriptions.sh
    └── pull_review_comments.sh
```

## Privacy

- The default `.gitignore` in `$VOICE_HOME` excludes `corpus/` and all `.jsonl` files. Sent email, work-repo PR comments, and Slack DMs are personal/employer-sensitive content; never commit them to a shared repo.
- If you want to back up your corpus, use a private personal repo and only commit if you're sure no employer-confidential content is included.

## Dependencies

- **pi** with this package installed
- **Gmail MCP** (optional; needed for `/skill:voice-pull-emails`). Any Gmail MCP server that exposes thread search and thread get with full content.
- **`gh` CLI** (optional; needed for the GitHub scripts). Authenticated against `github.com`. For private-org repos with SAML SSO, run `gh auth refresh -h github.com -s repo,read:org` and authorize for each org.
- **`jq`** (used by both shell scripts).

## Why not fine-tune?

Voice fine-tuning needs ~1000-5000+ register-matched samples to meaningfully shift base behavior; below that you mostly get overfitting to the *content* of the corpus. Most individuals have under 200 substantive samples across all sources combined, and refresh cadence is too slow to make iteration practical (rule change requires retraining, not a 30-second `rules.md` edit). The exception worth considering: a fine-tune as a *rewriter* (input: rough draft, output: voice-aligned), with a few thousand draft/final pairs. Even then, RAG over `voice.md` plus this package's grounding samples typically gets you 90% of the way at zero training cost.

## Relation to the Claude Code plugin

This is a faithful port of the `my-voice` Claude Code plugin. Same corpus layout, same templates, same scripts, same workflow. The only differences are harness-shaped:

- Claude `commands/*.md` → pi `skills/<name>/SKILL.md`, invoked as `/skill:voice*` instead of `/voice*`.
- Skill names drop the colon (`voice:init` → `voice-init`) per the pi/Agent-Skills name rules.
- `${CLAUDE_PLUGIN_ROOT}` template/script references → paths relative to the skill directory (`../../templates`, `../../scripts`).
