@sellable/mcp 0.1.31 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1064,11 +1064,17 @@ proof:`, `p.s. useful proof:`, `p.s. proof:`, or `p.s. social proof:`.
1064
1064
  `{{psLine}}`, and do not use `{{recent_signal_quote}}`; use fields that exist
1065
1065
  on the enriched prospect row instead.
1066
1066
  - the live body under `## Approved Message Template` must be sender-ready copy.
1067
- Do not include bracketed instruction placeholders such as `[ROW BRIDGE ...]`,
1068
- `[insert ...]`, `[generated ...]`, or prose that tells a later step to
1069
- paraphrase/fill a line. Put per-row generation rules in `## Token Fill Rules`
1070
- with concrete enriched-row fields, and copy approved good/bad examples into
1071
- `## Token Fill Examples`, or route to `revise-messaging`.
1067
+ Bracketed text in the body is allowed only when it is a fully-specified
1068
+ AI-native token (`[ALL_CAPS_NAME Intent. DO: ... DON'T: ... FALLBACK:
1069
+ omit the line.]` see
1070
+ `mcp/sellable/skills/create-campaign/references/ai-native-tokens.md` for the
1071
+ contract). Bracketed instruction placeholders that lack a contract are
1072
+ BLOCKED — including `[ROW BRIDGE ...]`, `[insert ...]`, `[generated ...]`,
1073
+ and any prose that defers per-row composition to a later step without
1074
+ giving the model the rules. Either upgrade the placeholder to a full
1075
+ AI-native token (Intent / DO / DON'T / FALLBACK), put the per-row rules in
1076
+ `## Token Fill Rules` with concrete enriched-row fields, or route to
1077
+ `revise-messaging`.
1072
1078
  - `approval-packet.md` and the live campaign brief passed to `create_campaign`
1073
1079
  must include `## Approved Message Template`, `## Token Fill Rules`, and
1074
1080
  `## Token Fill Examples`. `## Token Fill Examples` must copy the approved
@@ -1391,25 +1397,39 @@ Exact sequence:
1391
1397
  `campaignBrief.content` contains `{{...}}`; without this marker it will
1392
1398
  use full-generation mode and may rewrite the approved message.
1393
1399
  The live template body must contain only sender-ready copy plus supported
1394
- `{{tokens}}`; no bracketed instructions or placeholder prose may appear in
1395
- the body.
1396
- Include `## Token Fill Rules` for every enriched-prospect-row token in the
1397
- template. Tokens may be row fields such as `{{first_name}}`,
1398
- `{{company}}`, `{{role}}`, `{{headline}}`, `{{profile_summary}}`,
1399
- `{{post_context}}`, `{{comment_summary}}`, `{{source_post_topic}}`, or
1400
- `{{row_proof_note}}` when those exact fields are supported by the
1401
- row/enrichment data. Do not document static sender identity, product name,
1402
- proof points, casing style, `{{recent_signal_quote}}`, or abstract slot
1403
- tokens such as `{{hookLine}}`, `{{painLine}}`, `{{productLine}}`,
1404
- `{{closeLine}}`, or `{{psLine}}`.
1405
- Include `## Token Fill Examples` copied from the approved message review and
1406
- validation artifacts. It must preserve `Good token fill:`, `Good omit:`,
1407
- `Bad token fill:`, `Why bad:`, `Fallback if missing:`, and
1408
- `Token fill basis:` so the minted campaign brief teaches future row
1409
- generation how to fill tokens, what fills are blocked, and what to do when
1410
- row data is missing. If the brief does not contain `## Token Fill Rules` and
1411
- `## Token Fill Examples`, do not call `create_campaign`; route back to
1412
- message review or approval packet generation.
1400
+ tokens. Two token shapes are allowed:
1401
+ - `{{snake_case}}` field-substitution tokens for atomic values that drop
1402
+ directly into a fixed slot (greeting, subject, company name).
1403
+ - `[ALL_CAPS_NAME — Intent. DO: ... DON'T: ... FALLBACK: omit the line.]`
1404
+ AI-native tokens for personalization sentences. The bracket carries
1405
+ the contract inline; the model writes a sentence (or omits the line)
1406
+ per the inline rules. Full spec:
1407
+ `mcp/sellable/skills/create-campaign/references/ai-native-tokens.md`.
1408
+ No other bracketed placeholders are allowed bracketed prose without an
1409
+ explicit Intent / DO / DON'T / FALLBACK contract (e.g., `[ROW BRIDGE]`,
1410
+ `[insert ...]`, `[generated ...]`) is BLOCKED because it leaves the
1411
+ per-row generation under-specified.
1412
+ Include `## Token Fill Rules` for every token in the template. Field
1413
+ tokens may be row fields such as `{{first_name}}`, `{{company}}`,
1414
+ `{{role}}`, `{{headline}}`, `{{profile_summary}}`, `{{post_context}}`,
1415
+ `{{comment_summary}}`, `{{source_post_topic}}`, or `{{row_proof_note}}`
1416
+ when those exact fields are supported by the row/enrichment data. AI-native
1417
+ tokens are listed in the rules table by name and type with a pointer back
1418
+ to the inline contract (the rules themselves live inside the bracket — do
1419
+ not duplicate them in the table). Do not document static sender identity,
1420
+ product name, proof points, casing style, `{{recent_signal_quote}}`, or
1421
+ abstract slot tokens such as `{{hookLine}}`, `{{painLine}}`,
1422
+ `{{productLine}}`, `{{closeLine}}`, or `{{psLine}}` — those are abstract
1423
+ slots without a contract; convert them to AI-native bracket tokens with
1424
+ real Intent / DO / DON'T / FALLBACK clauses, or cut them.
1425
+ Include `## Token Fill Examples` copied from the approved message review and
1426
+ validation artifacts. It must preserve `Good token fill:`, `Good omit:`,
1427
+ `Bad token fill:`, `Why bad:`, `Fallback if missing:`, and
1428
+ `Token fill basis:` so the minted campaign brief teaches future row
1429
+ generation how to fill tokens, what fills are blocked, and what to do when
1430
+ row data is missing. If the brief does not contain `## Token Fill Rules` and
1431
+ `## Token Fill Examples`, do not call `create_campaign`; route back to
1432
+ message review or approval packet generation.
1413
1433
  3. Call `bootstrap_create_campaign({ flowVersion: "v2" })`. Respect
1414
1434
  `safeToProceed`; on blocking errors, surface and stop.
1415
1435
  4. Call
@@ -102,7 +102,6 @@
102
102
  "allowedTools": [
103
103
  "list_senders",
104
104
  "get_sender",
105
- "enrich_sender",
106
105
  "fetch_company",
107
106
  "fetch_linkedin_profile",
108
107
  "WebFetch",
@@ -174,7 +173,6 @@
174
173
  "get_active_workspace",
175
174
  "list_senders",
176
175
  "get_sender",
177
- "enrich_sender",
178
176
  "complete_sender_research",
179
177
  "fetch_company",
180
178
  "fetch_company_posts",
@@ -1139,6 +1139,44 @@ yours` are not enough when a safe row token would make the line feel more
1139
1139
  version sounds like mail merge. A good token should either make the sentence
1140
1140
  more concrete in normal language or be omitted. If the row signal is weak,
1141
1141
  write the segment-level line and document the omit rule.
1142
+ - **No product-noun substitution in possessive frames (HARD INVARIANT):** when
1143
+ a token sits inside a possessive frame like `your {{X}}`, `at your {{X}}`,
1144
+ or `because of your {{X}}`, the filled value MUST describe something the
1145
+ recipient personally **does** — their work, focus, or activity. It must NOT
1146
+ describe a product their company **builds or sells**. Grammar test: read
1147
+ `your <filled-value>` out loud. If it reads as if the recipient _uses_ or
1148
+ _has_ the thing, but they actually _build/sell_ the thing, the fill is wrong
1149
+ → OMIT the entire sentence. Examples: `your monetization layer for AI
1150
+ builders` (BLOCKED — product description from the company), `your Moneyball
1151
+ dashboard for CEOs` (BLOCKED — the recipient builds this product, doesn't
1152
+ have one of their own), `your AI scoring engine` (BLOCKED — product), vs
1153
+ `your monetization research for AI founders` (ALLOWED — buyer activity),
1154
+ `your founder-led-sales experiments` (ALLOWED — activity), `your GTM
1155
+ Engineering writing` (ALLOWED — what they do publicly). The omit-fallback
1156
+ is the safe default. Aggressive omit > awkward fill. Do NOT substitute
1157
+ generic nouns (`your work`, `your stack`, `your team`) — those add no
1158
+ relevance and read as mail merge.
1159
+ - **AI-native tokens — write the sentence, don't fill the slot (HARD
1160
+ INVARIANT):** when the message template contains a bracketed instruction
1161
+ token of the form \`[ALL_CAPS_NAME — instructions]\`, that placeholder is
1162
+ NOT a field-substitution slot. It is an inline instruction. Read the
1163
+ contents of the bracket. The bracket will name an Intent, DO shapes,
1164
+ DON'T shapes, and a FALLBACK clause. Your job: COMPOSE a complete
1165
+ sentence (or sentences, if the intent calls for it) that satisfies all
1166
+ the DO rules and avoids all the DON'T rules, then replace the bracket
1167
+ with that rendered sentence. If you cannot satisfy the DOs cleanly from
1168
+ the row data, REPLACE THE BRACKET WITH NOTHING (the entire line is
1169
+ omitted, including any leading/trailing whitespace; the surrounding
1170
+ message must read cleanly without it). Aggressive omit > awkward fill.
1171
+ Never leave the bracket itself in the rendered output. Never substitute
1172
+ a placeholder, ellipsis, or generic noun (\`your work\`, \`your team\`)
1173
+ when the FALLBACK clause says omit. Canonical example from the runtime
1174
+ gold pack: \`[PERSONALIZED REASON — their team size, role, or why
1175
+ they're a perfect fit]\` — the model picks whichever input the row supports
1176
+ and writes one short sentence in the sender's voice. Full spec:
1177
+ \`references/ai-native-tokens.md\`. The \`[ALL_CAPS_NAME — ...]\` shape
1178
+ is reserved for AI-native tokens; field substitutions stay as
1179
+ \`{{snake_case}}\` and continue to work as direct value injections.
1142
1180
  - **No internal profile-signal token:** `{{profile_signal}}` is never allowed
1143
1181
  in customer-facing copy, message-review templates, rendered examples, token
1144
1182
  notes, or approval-packet message bodies. It names how enrichment classified
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  name: research-sender
3
- description: Parallel-first sender research protocol. One round of batched tool calls, ~10-15s wall time.
3
+ description: "Parallel-first sender research protocol. One round of five batched tool calls (fetch_linkedin_profile + fetch_company + 3× WebSearch), no enrich_sender required, ~30-40s wall time."
4
4
  visibility: internal
5
+ allowed-tools: mcp__sellable__fetch_linkedin_profile mcp__sellable__fetch_company mcp__sellable__complete_sender_research WebSearch ToolSearch
5
6
  ---
6
7
 
7
8
  # Research Sender (Parallel-First)
@@ -12,28 +13,36 @@ This protocol is standalone and does not depend on the generic `research` subski
12
13
  ## Core Idea — Don't Wait, Fan Out
13
14
 
14
15
  Old protocol: call `enrich_sender` → wait for partial → maybe spawn 2-3 subagents → wait.
15
- That's 3-4 sequential rounds and ~60s+ wall time.
16
+ That's 3-4 sequential rounds and ~88s+ wall time.
16
17
 
17
- New protocol: **fire one parallel batch.** `enrich_sender` (5-8s, returns
18
- `clientProspectId` + baseline) runs alongside 2-3 `WebSearch` calls. They all
19
- finish in the same ~10s window, then you synthesize.
18
+ New protocol: **fire one parallel batch.** No `enrich_sender` precondition.
19
+ `fetch_linkedin_profile` (sender background), `fetch_company` (company description /
20
+ industry / employees), and `WebSearch` (proof, growth, positioning) all run in the
21
+ same window. They finish in ~10-15s if truly parallel, ~30-40s when the model
22
+ serializes (interactive mode is best-effort, headless `claude -p` always serializes).
20
23
 
21
- The `enrich_sender` call is still required because downstream tools
22
- (`create_campaign`, `attach_sequence`, message generation) need the
23
- `clientProspectId` it produces and the cached `EnrichedProspect` row it writes.
24
- Don't skip it — just stop waiting for it before doing the rest of the research.
24
+ Then synthesize once into the campaign brief — the brief is what downstream
25
+ message generation reads, not a `clientProspect` row. `clientProspectId` is
26
+ optional in `create_campaign`; pass `senderLinkedinUrl` instead.
25
27
 
26
28
  ## Inputs
27
29
 
28
30
  Provide as many as available. Use `"Unknown"` for missing values.
29
31
 
30
- - `linkedinUrl` (REQUIRED)
32
+ - `linkedinUrl` (REQUIRED — drives `fetch_linkedin_profile`)
31
33
  - `name`
32
34
  - `title`
33
35
  - `companyName`
34
- - `companyDomain` (REQUIRED for WebSearch derive from email or LinkedIn if absent)
36
+ - `companyDomain` (REQUIRED drives `fetch_company` and all three WebSearch queries; derive from email or LinkedIn if absent)
35
37
  - `headline` (optional)
36
38
 
39
+ ## Execution Backend Routing
40
+
41
+ This skill runs differently depending on the host:
42
+
43
+ - **Claude Code host:** the parallel batch uses MCP tools directly — `mcp__sellable__fetch_linkedin_profile`, `mcp__sellable__fetch_company`, and `WebSearch`. Issue all five calls as `tool_use` blocks in a single assistant message.
44
+ - **Codex host:** if MCP `fetch_*` tools are not available, fall back to web-fetch equivalents — use the LinkedIn URL with WebFetch for the sender profile, fetch the company's `/about` page for company facts, and use the same three WebSearch queries below for proof / growth / positioning. Synthesis treats both backends identically.
45
+
37
46
  ## Setup: Pre-Load Deferred Tools (One-Time, Before The Parallel Batch)
38
47
 
39
48
  Some hosts (Claude Code with deferred-tool fetch) require `WebSearch` and
@@ -41,27 +50,28 @@ some MCP tools to be loaded via `ToolSearch` before they can be invoked. If
41
50
  they are not already directly callable, issue this once as the first tool
42
51
  call, on its own:
43
52
 
44
- `ToolSearch({ query: "select:WebSearch,mcp__sellable__enrich_sender,mcp__sellable__complete_sender_research", max_results: 5 })`
53
+ `ToolSearch({ query: "select:WebSearch,mcp__sellable__fetch_linkedin_profile,mcp__sellable__fetch_company,mcp__sellable__complete_sender_research", max_results: 5 })`
45
54
 
46
55
  Skip this turn entirely if the tools are already directly callable.
47
56
 
48
57
  ## The One Round (Parallel Batch)
49
58
 
50
- **HARD RULE:** the four tool calls below MUST be emitted as **four `tool_use`
59
+ **HARD RULE:** the five tool calls below MUST be emitted as **five `tool_use`
51
60
  content blocks inside a single assistant message** (one turn, parallel
52
61
  execution). Do NOT split them across multiple assistant messages.
53
62
 
54
- Concretely, your next assistant message must contain exactly these four
63
+ Concretely, your next assistant message must contain exactly these five
55
64
  `tool_use` blocks, in any order, with no leading or trailing prose:
56
65
 
57
- 1. `enrich_sender({ linkedinUrl, companyDomain })` — baseline + `clientProspectId`.
58
- 2. `WebSearch({ query: 'site:{companyDomain} ("case study" OR "customer story" OR testimonial OR "success story") "{companyName}"' })` proof.
59
- 3. `WebSearch({ query: '"{companyName}" {companyDomain} {currentYear} (funding OR raised OR seed OR series OR hiring OR launch OR "press release")' })` — growth/credibility.
60
- 4. `WebSearch({ query: '"{companyName}" about product site:{companyDomain}' })` — positioning.
66
+ 1. `fetch_linkedin_profile({ linkedinUrl })` — sender's LinkedIn profile (firstName/lastName, currentCompany, experience, follower count, education) for the brief's "sender background" section.
67
+ 2. `fetch_company({ companyDomain })` sender's company LinkedIn page (description, industry, employee range, recent posts). REQUIRED — replaces what `enrich_sender` used to provide for `companySnapshot`.
68
+ 3. `WebSearch({ query: 'site:{companyDomain} ("case study" OR "customer story" OR testimonial OR "success story") "{companyName}"' })` — proof.
69
+ 4. `WebSearch({ query: '"{companyName}" {companyDomain} {currentYear} (funding OR raised OR seed OR series OR hiring OR launch OR "press release")' })` — growth/credibility.
70
+ 5. `WebSearch({ query: '"{companyName}" about product site:{companyDomain}' })` — positioning.
61
71
 
62
- **Self-check before you reply:** if your reply contains fewer than four
72
+ **Self-check before you reply:** if your reply contains fewer than five
63
73
  `tool_use` blocks (after the optional `ToolSearch` setup turn), you are
64
- violating the protocol. Stop, rewrite the reply with all four.
74
+ violating the protocol. Stop, rewrite the reply with all five.
65
75
 
66
76
  **Known limitation (`claude -p` headless mode):** Claude often serializes
67
77
  these calls one-per-turn even with explicit instructions. That's fine — the
@@ -69,35 +79,63 @@ skill still produces the same output, just with sequential tool execution
69
79
  (~30–40s total wall time vs. ~10–15s if truly parallelized). If you can
70
80
  batch, do; if not, proceed sequentially without retrying or re-prompting.
71
81
 
72
- No subagent fan-out. No second round of enrichment polling.
82
+ No subagent fan-out. No second round of enrichment polling. **No `enrich_sender` call.**
73
83
 
74
84
  ## Synthesis (Single Pass After Batch Returns)
75
85
 
76
- Merge the four results. Treat `enrich_sender` as the authoritative baseline
77
- for company/sender facts; treat WebSearch as proof and signals.
86
+ Merge the five results into the campaign brief. Treat `fetch_linkedin_profile` and
87
+ `fetch_company` as authoritative for sender/company facts; treat `WebSearch` as
88
+ proof and signals.
78
89
 
79
90
  ```markdown
80
91
  ## {Company} - Sender Research
81
92
 
82
- **Company Context:** [companySnapshot.description OR positioning WebSearch top result]
83
- **Industry / Size:** [companySnapshot.industry + employeeRange]
84
- **Positioning Notes:** [proofDigest.positioningOneLiner + keyDifferentiators]
85
- **Proof Options:** [proofDigest.caseStudySummary + case-study WebSearch hits pick 1-3 with named customers / metrics]
86
- **Credibility Signals:** [proofDigest.reviewHighlight + growth WebSearch hits (funding, hiring, press)]
93
+ **Sender Background:** [from fetch_linkedin_profile name, current title, follower count, key experience]
94
+ **Company Context:** [from fetch_company.description OR positioning WebSearch top result]
95
+ **Industry / Size:** [from fetch_company.industry + employee range]
96
+ **Positioning Notes:** [positioning WebSearch top results + any positioning one-liner from fetch_company]
97
+ **Proof Options:** [case-study WebSearch hits pick 1-3 with named customers / metrics]
98
+ **Credibility Signals:** [growth WebSearch hits (funding, hiring, press) + any high-signal posts from fetch_company]
87
99
  **Gaps:** [what's still missing — usually fine to ship with this]
88
100
  ```
89
101
 
90
- If `enrich_sender` returns `enrichmentStatus: "partial"`, that's expected and
91
- fine `companySnapshot` and `senderBackground` are populated immediately.
92
- `proofDigest` may be null on partial; in that case rely on WebSearch results
93
- for proof and credibility, not on a second enrichment poll.
102
+ The synthesis result is what feeds the campaign brief, which is what message
103
+ generation reads via `campaignBriefContent` (see `getOfferPositioning()` in
104
+ `prompt.ts:236`). The brief is the load-bearing path now `clientProspect`
105
+ contributes only fallback context and the sender display name (which is
106
+ resolved from the `Sender` row's profileData when `clientProspect` is null).
107
+
108
+ ### Partial Failure Handling
109
+
110
+ The five calls run in parallel, so any one of them can fail independently
111
+ (404, rate-limit, timeout). Synthesis MUST still produce a usable brief.
112
+ Apply these rules:
113
+
114
+ - **If `fetch_company` errors or returns empty:** mark `Company Context` as
115
+ `[unavailable — fetch_company failed]`. Use the positioning WebSearch top
116
+ result as a partial substitute. Set `Industry / Size` to `[unavailable]`
117
+ (do NOT guess). The brief still ships; downstream message generation reads
118
+ the brief verbatim and is null-safe on missing fields.
119
+ - **If `fetch_linkedin_profile` errors:** fall back to WebSearch-derived
120
+ sender background. Set `Sender Background` to a one-liner derived from the
121
+ positioning WebSearch ("found via {companyDomain}/about and other web
122
+ signals — full LinkedIn profile unavailable").
123
+ - **If a WebSearch errors:** mark the corresponding section
124
+ (`Proof Options`, `Credibility Signals`, or `Positioning Notes`) as `[no
125
+ results]` and continue. Do NOT retry — one round only.
126
+ - **NEVER skip `complete_sender_research`** even on partial failure. Pass
127
+ `proofItemsFound: 0`, `caseStudyItemsFound: 0`, `credibilitySignalsFound: 0`
128
+ for the failed sections so the preflight knows the research ran.
129
+
130
+ **The contract:** synthesis always produces SOME brief. `complete_sender_research`
131
+ always fires. The brief ships even with gaps; downstream is null-safe.
94
132
 
95
133
  ## Optional Deepen (Only If Synthesis Reveals A Hard Gap)
96
134
 
97
135
  If after synthesis you genuinely have zero proof and zero credibility
98
136
  signals, AND the campaign fixture/operator told you proof is required, you
99
137
  MAY issue ONE additional WebFetch on the most promising case-study URL from
100
- batch step 2. Cap at one WebFetch. No subagents, no second WebSearch round.
138
+ the proof WebSearch. Cap at one WebFetch. No subagents, no second WebSearch round.
101
139
 
102
140
  If that still yields nothing, set `proofItemsFound: 0` in
103
141
  `complete_sender_research` and proceed; the brief can ship without proof.
@@ -122,7 +160,7 @@ If no reliable evidence is found, set counts to 0 and include that in `notes`.
122
160
  Before issuing the parallel batch:
123
161
 
124
162
  ```
125
- Pulling sender enrichment + case studies + growth signals + positioning in parallel (~10s)...
163
+ Pulling sender profile + company snapshot + case studies + growth signals + positioning in parallel (~10s)...
126
164
  ```
127
165
 
128
166
  After synthesis:
@@ -133,7 +171,11 @@ Sender research ready — {proofItemsFound} proof items, {credibilitySignalsFoun
133
171
 
134
172
  ## What Changed From The Previous Protocol
135
173
 
174
+ - **Removed `enrich_sender` from the parallel batch entirely.** The MCP `create_campaign` tool now accepts `senderLinkedinUrl` as an alternative to `clientProspectId`. `enrich_sender` itself has been removed from the MCP tool registry — calling it returns "tool not found".
175
+ - Replaced with `fetch_company` (REQUIRED) — provides company description, industry, employee data that `enrich_sender` used to give via `companySnapshot`.
176
+ - Five parallel tool calls instead of four (added `fetch_linkedin_profile` + `fetch_company`, dropped `enrich_sender`).
177
+ - Added explicit Partial Failure Handling — synthesis always produces a brief even when one or two of the five calls error.
136
178
  - No "minimal-verification vs deep-proof" branch. Always run the same parallel batch.
137
- - No spawn of `general-purpose` subagents for research — WebSearch from the orchestrator turn is faster and cheaper.
138
- - No waiting on `enrich_sender` before issuing WebSearch.
179
+ - No subagent fan-out — WebSearch from the orchestrator turn is faster and cheaper.
180
+ - Synthesis writes to the campaign brief, not to an `EnrichedProspect` row.
139
181
  - One round, one synthesis, one `complete_sender_research` call.