Files
2026-04-02 20:55:07 +08:00

251 lines
9.6 KiB
Markdown

---
name: reply-handler
version: 2.0.0
description: |
Draft a reply to a LinkedIn prospect's message. Uses shared state, classification,
and scripts for artifacts and metrics. Trigger on: "draft a reply", "they responded",
or when the user pastes a prospect message.
allowed-tools:
- Read
- Write
- AskUserQuestion
- Bash
---
## Voice
Responsive, not scripted. Every reply should feel like it was written by someone who actually read the message. No boilerplate. No repositioning as a pitch. Pull the real thread forward.
## Completion Status Protocol
When completing a skill workflow, report status using one of:
- **DONE** — Reply drafted. Reply type and strategy noted. Artifact written if script ran.
- **DONE_WITH_CONCERNS** — Drafted, but the prospect's intent was ambiguous. Note what to watch for.
- **BLOCKED** — Cannot draft without more context. State what's missing.
- **NEEDS_CONTEXT** — Missing voice profile or conversation history. State exactly what you need.
---
## Required shared standards
Read before running:
- `standards/skill-method.md`
- `state/linkedin-settings.json`
- `state/linkedin-system-state.json`
Voice: use whatever voice context exists in the conversation (e.g., from a prior `/calibrate-voice` run). If none, proceed with neutral professional tone.
---
## Step 0: Resolve lead identity
**First, check the registry for known leads.**
Read `state/linkedin-system-state.json`. Look for leads in `outreachRegistry` with `lastDraftStatus` of `contacted` or `drafted`.
If contacted leads exist, present them as a numbered list and ask which one replied:
```
Who replied? Pick a number or paste their message to auto-match:
1. [Name] @ [Company] — sent [lastDraftAt date]
2. [Name] @ [Company] — sent [lastDraftAt date]
...
Or type a name if they're not on this list.
```
Use the selected lead's existing `lead_key` — do not ask for name/company/URL again.
**Fallback (no registry or no contacted leads):**
Ask once for name + company + LinkedIn URL to build `lead_key`:
`lowercase(trim(name) + "|" + trim(company) + "|" + trim(linkedin_url))`
Or accept `--lead-key` directly if provided.
---
## Step 1: Gather conversation context
The user should provide:
- The prospect's message (paste it in full)
- Any prior messages in the thread (optional but helpful)
- Campaign goal (if set): book a call, share a resource, qualify interest, etc.
If the goal is not provided, assume: move toward a short discovery conversation.
---
## Step 2: Classify the reply
Read the prospect's message and classify it:
| Reply type | Signals |
|------------|---------|
| `interested` | Asking for more info, asking how it works, expressing curiosity |
| `hesitant` | "Not the right time", "too busy", "maybe later", vague deflection |
| `competitor_mention` | Names another tool they use, implies the problem is already solved |
| `positive_but_vague` | "Sounds interesting", "tell me more" with no specific question |
| `not_interested` | Explicit no, asking to be removed, unsubscribing |
| `question` | Direct question about the product, pricing, or process |
| `irrelevant` | Off-topic, misdirected, or unclear |
State the classification before drafting.
---
## Step 2.5: Show reply logic and confirm before drafting
After classifying, show the user exactly how you plan to reply and why — then wait for confirmation or edits before writing the message.
Format:
```
REPLY LOGIC FOR: [Name] @ [Company]
─────────────────────────────────────
Their message: [1-sentence summary of what they said]
Reply type: [classification]
Rule applied: [the specific rule that governs this reply type, in plain English]
Opening angle: [what the reply will lead with]
Campaign goal: [exact goal from outreach.conversionGoal in settings — e.g. "book a discovery call", "get them to try the free plan"]
Voice applied: [2-3 key traits pulled from context/brand-voice.md — e.g. "direct opener, no filler words, ends with one low-friction question"]
URL to include: [yes — [url from outreach.conversionGoal.link] / not yet — will include after [trigger]]
Rules I'll follow for this reply:
1. No em dash — use commas or periods instead
2. Every sentence under 30 words
3. [No pitch — follow-up, not yet at pitch stage / Pitch allowed — prospect has shown clear interest]
4. Peer-to-peer tone — no flattery, no bragging
5. Max 1 question
6. [Pulls thread from their specific message: [what exactly] / No new signal needed — responding to their question directly]
7. [Graceful exit — acknowledge, leave door open, no push / N/A — prospect is still engaged]
Any rules to add, remove, or override before I write this?
```
Wait for the user to confirm or edit before proceeding to Step 3.
---
## Step 3: Productized pipeline (script)
Check if Node.js is available:
```bash
command -v node >/dev/null 2>&1 && echo "NODE_AVAILABLE" || echo "NO_NODE"
```
If NODE_AVAILABLE, run from repo root:
```bash
node scripts/bootstrap-system.js
node scripts/run-reply-handler.js --lead-key "<canonical lead_key>" --message "paste prospect message"
```
Optional: long messages from file:
```bash
node scripts/run-reply-handler.js --lead-key "<key>" --message-file path/to/prospect.txt
```
The script writes `output/replies_YYYY_MM_DD/reply_<lead>_<date>.json`, updates `outreachRegistry`, and increments `weeklyMetrics.reply_drafts_count`.
If NO_NODE: skip the script. Claude will write the artifact and update state directly using the Write tool.
Then **refine** the draft in your voice using `context/brand-voice.md`. The script output is a structured starting point, not the final send.
---
## Step 4: Draft the reply (agent quality pass)
Apply the branching logic based on reply type (same rules as v1).
**If `interested`:**
Ask a qualifying question. One question only. Do not pitch yet. The goal is to understand their specific situation. Do not send the landing page URL in this message.
**If `hesitant`:**
Acknowledge what they said. Ask what's making it feel like the wrong time. Do not push harder. Pull out the real objection. One gentle question is enough.
**If `competitor_mention`:**
Do not position against the competitor by name. Ask if that tool is solving the specific problem they mentioned. Let them arrive at the contrast themselves. Never say "compared to X, we..."
**If `positive_but_vague`:**
Deliver value first. Share the landing page (use the URL from `outreach.conversionGoal.link` in settings, if set). Frame it as "here's where you can see exactly how it works" or similar — not as a pitch. Then mention a call is available if they have questions, but do not include the booking link or suggest a time. Only share the calendar/booking link if they explicitly ask to schedule. Keep the message short.
**If `not_interested`:**
Thank them. Leave the door open. No follow-up pressure. Do not ask why. The reply should feel like a graceful exit, not a save attempt.
**If `question`:**
Answer directly. If it's a pricing question, give a real answer or offer to discuss on a call. If it's a fit question, answer honestly. Do not stall or redirect before answering.
**If `irrelevant`:**
Politely redirect to the relevant topic in one sentence.
---
## Step 5: Annotate the reply
After the draft:
```
DRAFT: [the message]
Reply type: [classification]
Strategy: [1 sentence on why this approach]
Next trigger: [what response from them would prompt the next step]
URL to include: [yes / not yet — and when to include it]
```
Then immediately output this options block:
```
Your options:
- Send it — say "sent" and I'll log the outcome
- Another reply — say "reply from [name]" to handle the next one
- More leads — say "draft outreach" to continue with uncontacted leads
- Wrap up the campaign — run /campaign-retro to analyze what worked
```
---
## Step 6: Record outcome (when user reports what happened)
When the user shares an actual result, persist metrics if state files exist:
If NODE_AVAILABLE:
```bash
node scripts/record-outcome.js --lead-key "<canonical lead_key>" --outcome positive
```
If NO_NODE: update `state/linkedin-system-state.json` directly — increment the relevant counter and set `outreachRegistry[lead_key].lastDraftStatus` to the outcome value.
Valid `--outcome` values:
- `positive` — warm reply or clear interest
- `rejected` — soft no / not a fit
- `not_interested` — hard no / unsubscribe tone
- `booked` — meeting or call booked
If `state/linkedin-system-state.json` does not exist, skip the script and note in the report that metrics were not persisted. This does not block the reply draft from being useful.
---
## Step 7: Handoff rules
If the campaign has a handoff level configured, note when to hand off to the user:
- **Partial:** draft all replies for user approval until the prospect asks to schedule
- **Full AI:** proceed autonomously, hand off only when a call is booked
- **Manual:** always draft for approval
Default to partial if not specified.
---
## Important Rules
- Never ask more than one question per message. One thread at a time.
- Never repeat the prospect's name in the reply.
- For `positive_but_vague`: include the landing page URL (from `outreach.conversionGoal.link`) to deliver value. Do not include the booking/calendar link unless the prospect asks.
- For all other reply types: do not include the landing page URL until the prospect asks or the conversation has progressed past initial interest.
- If the prospect's message has genuine ambiguity (could be hesitant or interested), classify conservatively and note the uncertainty.
- Always prioritize providing real value over pushing toward conversion.