@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.
@@ -97,8 +97,8 @@ export const promptToolDefinitions = [
97
97
  properties: {
98
98
  depth: {
99
99
  type: "string",
100
- enum: ["minimal-verification", "deep-proof"],
101
- description: "Research depth used: minimal verification or deeper proof-focused pass.",
100
+ enum: ["minimal-verification", "deep-proof", "parallel-batch"],
101
+ description: "Research depth used: minimal-verification (proofDigest already had strong signals), deep-proof (extra search agents spawned), or parallel-batch (Phase 114+ default — single 5-call parallel batch with fetch_linkedin_profile + fetch_company + 3x WebSearch).",
102
102
  },
103
103
  proofItemsFound: {
104
104
  type: "number",
@@ -20,6 +20,7 @@ export declare function resolveWaitTimeout(timeoutMs?: number): {
20
20
  effectiveTimeoutMs: number;
21
21
  guardApplied: boolean;
22
22
  };
23
+ export declare function hasNonWaitableMissing(missing: string[]): boolean;
23
24
  export declare const readinessToolDefinitions: ({
24
25
  name: string;
25
26
  description: string;
@@ -32,9 +32,11 @@ export function resolveWaitTimeout(timeoutMs) {
32
32
  guardApplied: requestedTimeoutMs > effectiveTimeoutMs,
33
33
  };
34
34
  }
35
- function hasNonWaitableMissing(missing) {
35
+ export function hasNonWaitableMissing(missing) {
36
+ // Phase 114: clientProspectId is no longer non-waitable. Net-new campaigns
37
+ // can be created with senderLinkedinUrl instead, with downstream resolution
38
+ // happening at send-time.
36
39
  const nonWaitable = new Set([
37
- "clientProspectId",
38
40
  "campaignBrief.content",
39
41
  "leadSourceType",
40
42
  'leadSourceType="new"',
@@ -2,6 +2,7 @@ export type CreateWorkflowTableInput = {
2
2
  name: string;
3
3
  senderId: string;
4
4
  clientProspectId?: string;
5
+ senderLinkedinUrl?: string;
5
6
  sequenceActions?: string[];
6
7
  };
7
8
  export type CreateWorkflowTableResponse = {
@@ -58,6 +59,10 @@ export declare const sequencerToolDefinitions: ({
58
59
  type: string;
59
60
  description: string;
60
61
  };
62
+ senderLinkedinUrl: {
63
+ type: string;
64
+ description: string;
65
+ };
61
66
  sequenceActions: {
62
67
  type: string;
63
68
  items: {
@@ -93,6 +98,7 @@ export declare const sequencerToolDefinitions: ({
93
98
  name?: undefined;
94
99
  senderId?: undefined;
95
100
  clientProspectId?: undefined;
101
+ senderLinkedinUrl?: undefined;
96
102
  sequenceActions?: undefined;
97
103
  campaignId?: undefined;
98
104
  };
@@ -115,6 +121,7 @@ export declare const sequencerToolDefinitions: ({
115
121
  name?: undefined;
116
122
  senderId?: undefined;
117
123
  clientProspectId?: undefined;
124
+ senderLinkedinUrl?: undefined;
118
125
  sequenceActions?: undefined;
119
126
  tableId?: undefined;
120
127
  template?: undefined;
@@ -7,7 +7,8 @@ export const sequencerToolDefinitions = [
7
7
  "All tables include LinkedIn URL, Name, and Approved. " +
8
8
  "Additional columns depend on the planned actions: INVITE/DM -> Message, " +
9
9
  "INMAIL_* -> Subject + Message, COMMENT -> Post URL + Comment + Reaction Type. " +
10
- "Campaign brief, rubrics, positioning, and prospect linkage remain optional follow-on metadata.",
10
+ "Campaign brief, rubrics, positioning, and prospect linkage remain optional follow-on metadata.\n\n" +
11
+ "INPUTS: Pass EITHER clientProspectId OR senderLinkedinUrl. Both optional — if neither is supplied, the table is created without prospect linkage.",
11
12
  inputSchema: {
12
13
  type: "object",
13
14
  properties: {
@@ -21,7 +22,11 @@ export const sequencerToolDefinitions = [
21
22
  },
22
23
  clientProspectId: {
23
24
  type: "string",
24
- description: "Optional client prospect ID (from enrich_sender)",
25
+ description: "Optional. EnrichedProspect ID. Pass this OR senderLinkedinUrl.",
26
+ },
27
+ senderLinkedinUrl: {
28
+ type: "string",
29
+ description: "Optional. Sender's LinkedIn profile URL — used to lazy-resolve the prospect at send-time when clientProspectId is not yet available.",
25
30
  },
26
31
  sequenceActions: {
27
32
  type: "array",
@@ -87,6 +92,7 @@ export async function createWorkflowTable(input) {
87
92
  name: input.name,
88
93
  senderId: input.senderId,
89
94
  clientProspectId: input.clientProspectId,
95
+ senderLinkedinUrl: input.senderLinkedinUrl,
90
96
  ...(input.sequenceActions
91
97
  ? { sequenceActions: input.sequenceActions }
92
98
  : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -4,6 +4,8 @@ description: Create a Sellable campaign through the approval-gated workflow.
4
4
  visibility: public
5
5
  allowed-tools:
6
6
  - mcp__sellable__get_auth_status
7
+ - mcp__sellable__start_cli_login
8
+ - mcp__sellable__wait_for_cli_login
7
9
  - mcp__sellable__bootstrap_create_campaign
8
10
  - mcp__sellable__get_subskill_prompt
9
11
  - mcp__sellable__search_subskill_prompts
@@ -323,14 +325,89 @@ plugin cache paths, MCP namespaces, or runbooks in customer-facing progress
323
325
  updates.
324
326
 
325
327
  1. Call `mcp__sellable__get_auth_status({})`.
326
- 2. If auth is not OK, stop and show the returned guidance.
327
- 3. Detect optional campaign id in the user request (`cmp_...`).
328
- 4. If no campaign id is provided, stay in fresh-create mode and do not call campaign discovery/resume helpers to find one.
328
+ 2. If auth is not OK with `error.type === "config"` or `error.type === "auth"`,
329
+ the user has not signed in yet. Run the FTUX magic-link handoff:
330
+
331
+ a. Say to the user verbatim:
332
+
333
+ ```text
334
+ Welcome to Sellable. What's your email?
335
+ ```
336
+
337
+ b. Wait for the user to paste their email in normal chat. Do NOT use
338
+ `AskUserQuestion` / `request_user_input` for this — it's free-text input.
339
+
340
+ c. Call `mcp__sellable__start_cli_login({ email })` with the email the user
341
+ typed.
342
+
343
+ d. If `start_cli_login` returns `ok: false`, surface `error.guidance` to the
344
+ user and stop. Do not retry automatically.
345
+
346
+ e. On `ok: true`, say to the user verbatim (substituting the email exactly
347
+ as the user typed it):
348
+
349
+ ```text
350
+ Magic link sent to {email}. Click it from your inbox — I'll wait. (If your team already has a Sellable workspace, ask an admin to invite you instead — that gets you straight into their data.)
351
+ ```
352
+
353
+ f. Call `mcp__sellable__wait_for_cli_login({ sessionId })` using the
354
+ `sessionId` returned by `start_cli_login`.
355
+
356
+ - If the result is `error.type === "tool_timeout_guard"`, IMMEDIATELY
357
+ re-call `mcp__sellable__wait_for_cli_login({ sessionId })` with the
358
+ SAME sessionId. Do not narrate anything to the user. Do not call
359
+ `start_cli_login` again — that would send a new magic link and confuse
360
+ them. Loop on `tool_timeout_guard` until you get a different result.
361
+
362
+ - If `error.type === "expired"` or `error.type === "timeout"`, say to the
363
+ user verbatim and stop:
364
+
365
+ ```text
366
+ That magic link expired. Run /sellable:create-campaign again to retry.
367
+ ```
368
+
369
+ - If `error.type === "already_consumed"` or any other error, surface
370
+ `error.guidance` and stop.
371
+
372
+ - On `ok: true`, the user is signed in and `~/.sellable/config.json` has
373
+ been written. Your IMMEDIATE next visible message branches on
374
+ `isReturningUser` from the tool result:
375
+
376
+ - If `isReturningUser === true`, prepend ONE line acknowledging the
377
+ reused workspace, then the locked Step 3 narration verbatim
378
+ (substituting `activeWorkspaceName` exactly):
379
+
380
+ ```text
381
+ You're in — using your {activeWorkspaceName} workspace.
382
+
383
+ Now — paste the LinkedIn profile URL of the person you want to send from.
384
+ ```
385
+
386
+ - If `isReturningUser === false`, prepend ONE line confirming the new
387
+ workspace, then the locked Step 3 narration verbatim:
388
+
389
+ ```text
390
+ Welcome to Sellable — created {activeWorkspaceName} for you.
391
+
392
+ Now — paste the LinkedIn profile URL of the person you want to send from.
393
+ ```
394
+
395
+ No other lines. No "all set", no "signed in", no other acknowledgement.
396
+
397
+ After the user pastes the URL, proceed with the existing identity-first
398
+ sender flow (Step 3 onwards in the v2 subskill prompt — sender
399
+ enrichment via `fetch_linkedin_profile` / `enrich_sender`).
400
+
401
+ 3. If auth is not OK with `error.type === "workspace"` (token valid, no active
402
+ workspace), stop and show the returned guidance — that's not a fresh-user
403
+ scenario; the user needs to run `set_active_workspace`.
404
+ 4. Detect optional campaign id in the user request (`cmp_...`).
405
+ 5. If no campaign id is provided, stay in fresh-create mode and do not call campaign discovery/resume helpers to find one.
329
406
  - Do not call `mcp__sellable__get_campaigns`.
330
407
  - Do not call `mcp__sellable__get_campaign` to hunt for IDs.
331
408
  - Do not call `mcp__sellable__create_campaign({ campaignId: ... })` unless the user supplied that id.
332
- 5. Call `mcp__sellable__bootstrap_create_campaign({ flowVersion: "v2", campaignId? })`.
333
- 6. If `safeToProceed !== true`, stop and show `blockingErrors` + `nextStep`.
409
+ 6. Call `mcp__sellable__bootstrap_create_campaign({ flowVersion: "v2", campaignId? })`.
410
+ 7. If `safeToProceed !== true`, stop and show `blockingErrors` + `nextStep`.
334
411
 
335
412
  ## Execute Workflow
336
413
 
@@ -0,0 +1,266 @@
1
+ # AI-Native Tokens
2
+
3
+ The canonical spec for personalization tokens in Sellable campaign messages.
4
+
5
+ ## Why this exists
6
+
7
+ Old-school personalization tokens (`{{first_name}}`, `{{company}}`,
8
+ `{{workflow_context}}`) are field substitutions: a string is looked up and
9
+ spliced into a fixed sentence. That works for atomic facts (first name,
10
+ company name). It fails for anything richer — because the _grammatical shape_
11
+ of the surrounding sentence locks in what the substituted value must look
12
+ like, and the rules about what makes a good substitution live in a separate
13
+ "Token Fill Rules" table that the generation model can easily ignore or
14
+ forget.
15
+
16
+ Concrete failure mode this caused in production: the template
17
+ `Thought of {{company}} because of your {{workflow_context}}.` was filled
18
+ with `Moneyball dashboard for CEOs` for a prospect who _built_ that product
19
+ at his company. The rendered line —
20
+ `Thought of Hatchproof because of your Moneyball dashboard for CEOs.` —
21
+ read as if we mistook the founder for a customer of his own product.
22
+ Field-substitution tokens have no built-in defense against that.
23
+
24
+ **AI-native tokens fix this.** The token _names what job the line does_ and
25
+ carries its rules _inline_. The model writes a SENTENCE (not fills a slot)
26
+ that satisfies the inline contract, or omits the line entirely. The grammar
27
+ trap disappears because the model is composing the line from scratch instead
28
+ of plugging a noun into a fixed frame.
29
+
30
+ ## The contract
31
+
32
+ An AI-native token is a bracketed instruction in the message template:
33
+
34
+ ```text
35
+ [ALL_CAPS_NAME — instructions describing what the line should do, with
36
+ DO / DON'T rules and a FALLBACK clause]
37
+ ```
38
+
39
+ Three properties define it:
40
+
41
+ 1. **Named by intent, not by field.** `[PERSONALIZATION_LINE — ...]` says
42
+ what JOB the line does. `{{workflow_context}}` only names a slot. When
43
+ the model sees an intent name, it has a target to write toward.
44
+ 2. **Rules live inline.** DO / DON'T / FALLBACK appear inside the bracket,
45
+ adjacent to where the line gets written. The model doesn't have to
46
+ consult a faraway rules table and remember to apply it.
47
+ 3. **Sentence-shaped, not noun-shaped.** The model produces an entire
48
+ sentence (or no sentence). There is no fixed frame around the token, so
49
+ no grammar trap. A weak fill is still a complete sentence; a wrong fill
50
+ is detectable at the sentence level.
51
+
52
+ The bracket is replaced at generation time by either (a) the rendered
53
+ sentence, or (b) nothing (omit). The brackets themselves never appear in
54
+ the final message.
55
+
56
+ ## Required clauses inside an AI-native token
57
+
58
+ Every well-specified AI-native token must include four things:
59
+
60
+ - **Intent.** One sentence on what job this line does in the message. Why
61
+ the line exists. What it should accomplish for the buyer.
62
+ - **DO.** 2-4 specific shapes the model should produce. Verb-led
63
+ prescriptions. Examples that work.
64
+ - **DON'T.** 2-4 anti-patterns the model must reject. Failure modes
65
+ observed in real campaigns. Bad shapes paired with WHY they're bad
66
+ when the bad-ness isn't obvious from the shape alone.
67
+ - **FALLBACK.** What to do when none of the DO shapes can be satisfied
68
+ cleanly from the row data. The default fallback is OMIT THE ENTIRE LINE.
69
+ Aggressive omit beats awkward fill, every time. Never silently
70
+ substitute a generic noun.
71
+
72
+ A token without all four clauses is under-specified. The model will
73
+ hallucinate the missing rules from priors, and those priors include
74
+ generic mail-merge failure modes.
75
+
76
+ ## The canonical example
77
+
78
+ This is the original Sellable gold-standard message. It uses two AI-native
79
+ tokens. Both have the contract baked in (loosely — even an under-specified
80
+ intent-name dramatically beats a field token, but the formal contract makes
81
+ it bulletproof):
82
+
83
+ ```text
84
+ hey [name],
85
+
86
+ saw you raise your hand for claude + gtm (creepy to reach out based on
87
+ that, i know) - but this felt too on the nose to ignore.
88
+
89
+ i'm building sellable, the only gtm platform that runs natively on claude
90
+ code.
91
+
92
+ we're looking for design partners - and [PERSONALIZED REASON - their team
93
+ size, role, or why they're a perfect fit].
94
+
95
+ two options:
96
+
97
+ a) 15-min call - i'll show you how you could book more meetings with
98
+ [THEIR ICP - who they want to reach], and if you like it we launch a pilot
99
+ right there
100
+
101
+ b) i send you the video of me using sellable to write and send this exact
102
+ message to you (yes, it's that meta)
103
+
104
+ p.s. yes, this message was entirely written and sent via claude code 😊
105
+ ```
106
+
107
+ Note what the tokens DO:
108
+
109
+ - `[PERSONALIZED REASON - their team size, role, or why they're a perfect fit]`
110
+ — the brackets name an intent (PERSONALIZED REASON) and offer three
111
+ acceptable input sources (team size, role, fit reason). The model picks
112
+ whichever is supported by the row.
113
+ - `[THEIR ICP - who they want to reach]` — names what the slot is FOR
114
+ ("their ICP") and clarifies in plain English ("who they want to reach"),
115
+ so the model doesn't need a glossary.
116
+
117
+ Both feel hand-crafted in every rendered version because the model is
118
+ _writing_, not _filling_.
119
+
120
+ ## The fully-specified contract (use this for new campaigns)
121
+
122
+ When authoring a new brief, write tokens in the full contract form. The
123
+ canonical Sellable example is loose because it predates the contract; it
124
+ still works because the intent names are crystal clear, but new tokens
125
+ should err toward explicit:
126
+
127
+ ```text
128
+ [PERSONALIZATION_LINE — write ONE short sentence (8-15 words) that anchors
129
+ this message to {{first_name}} as a person. Name what they personally DO,
130
+ write about, focus on, or build TOWARD (not what their company SELLS).
131
+ DO: use verb-led shapes — "Saw your work helping X", "Your writing on Y
132
+ caught my eye", "Your focus on Z is rare in this segment".
133
+ DON'T: name a product their company sells ("your dashboard for X", "your
134
+ platform for Y") — they BUILD the product, they don't have one.
135
+ DON'T: use source-citation phrasing ("Saw your post about", "Your bio
136
+ says").
137
+ DON'T: use generic noun substitutes ("your work", "your stack", "your
138
+ team") — those add no relevance.
139
+ FALLBACK: if you can't satisfy the DOs cleanly from the row data, OMIT
140
+ this entire line. The message reads cleanly without it. Aggressive omit >
141
+ awkward fill.]
142
+ ```
143
+
144
+ That bracket is ~15 lines. It produces ONE sentence in the rendered
145
+ message (or zero). The verbosity inside the bracket is the price of
146
+ predictable output across thousands of rows.
147
+
148
+ ## The grammar test
149
+
150
+ Every personalization line should pass this test before rendering: read
151
+ `your <X>` (or whatever possessive frame the line uses) out loud. If "X"
152
+ reads as if the recipient _uses_ or _has_ the thing, but the recipient
153
+ actually _builds_ or _sells_ the thing, the fill is wrong → OMIT.
154
+
155
+ Examples:
156
+
157
+ | Filled value | Pass test? |
158
+ | ----------------------------------------- | ------------------------------------ |
159
+ | `your monetization work for AI founders` | ✅ recipient does this work |
160
+ | `your founder voice on team performance` | ✅ recipient has this voice |
161
+ | `your Moneyball dashboard for CEOs` | ❌ recipient _builds_ this dashboard |
162
+ | `your monetization layer for AI builders` | ❌ recipient _sells_ this product |
163
+ | `your AI scoring engine` | ❌ product they sell |
164
+ | `your work` | ❌ generic, adds no relevance |
165
+ | `your team` | ❌ generic, adds no relevance |
166
+
167
+ When in doubt, omit. The message must read cleanly without the
168
+ personalization line. If it doesn't, the rest of the message is
169
+ under-specified — fix that, don't paper over it with weak personalization.
170
+
171
+ ## Field-substitution tokens still have a role
172
+
173
+ AI-native tokens replace personalization sentences. They do NOT replace
174
+ atomic field substitutions like `{{first_name}}` and `{{company}}`. Use
175
+ field tokens when:
176
+
177
+ - The value is an atomic noun that goes in a fixed slot (greeting:
178
+ `Hey {{first_name}},`, subject: `{{company}} outbound + ...`)
179
+ - The value comes directly from a row column with no judgment required
180
+ - Failure mode is "missing field" not "wrong shape" — and the fallback is
181
+ trivial (`there` instead of first name, omit the soft-bridge instead of
182
+ company)
183
+
184
+ Use AI-native tokens when:
185
+
186
+ - The value is a SENTENCE that requires judgment about what to write
187
+ - The value depends on synthesizing multiple row fields (bio + recent
188
+ posts + company context) into a human-sounding line
189
+ - Failure mode is "wrong shape" or "awkward grammar" — needs rules to
190
+ prevent
191
+ - The line is optional (you'd rather have NO line than a bad line)
192
+
193
+ Most campaigns will use both: field tokens for greeting/subject/company,
194
+ AI-native tokens for any personalization or context-bridging lines.
195
+
196
+ ## Brief authoring guide
197
+
198
+ When writing a campaign brief, document each token in two places:
199
+
200
+ ### 1. In the message template itself
201
+
202
+ Inline AI-native tokens go directly in the template body:
203
+
204
+ ```text
205
+ Hey {{first_name}},
206
+
207
+ [FIRST_LINE — opener that anchors to {{company}}'s current situation.
208
+ DO: ... DON'T: ... FALLBACK: ...]
209
+
210
+ [BODY — ... ]
211
+ ```
212
+
213
+ ### 2. In the Token Fill Rules section
214
+
215
+ A small table cataloging every token, marked by type:
216
+
217
+ ```markdown
218
+ | Token | Type | Source / Instructions | Fallback |
219
+ | ------------------------------ | --------- | --------------------------------------------- | ----------------------- |
220
+ | `{{first_name}}` | Field | Sales Nav `firstName` | `there` |
221
+ | `{{company}}` | Field | Sales Nav `organization.name`; strip suffixes | omit dependent sentence |
222
+ | `[PERSONALIZATION_LINE — ...]` | AI-native | Inline. See bracket. | Omit the line. |
223
+ ```
224
+
225
+ The AI-native row in the table doesn't need to repeat the instructions —
226
+ they live inline. The table just confirms the token exists, what type it
227
+ is, and where the fallback resolves.
228
+
229
+ ## Common authoring mistakes
230
+
231
+ - **Naming the token by source field instead of intent.**
232
+ `[LINKEDIN_BIO_PHRASE — ...]` is bad. `[PERSONALIZATION_LINE — ...]` is
233
+ good. The intent name tells the model what job to do; the source field
234
+ tells the model where to look but not what good looks like.
235
+ - **Rules in a separate table, intent in the template.** Keep them
236
+ together. The model's attention budget is finite — separating intent
237
+ from rules invites the model to forget the rules.
238
+ - **No FALLBACK clause.** Without it, the model assumes a fill is always
239
+ required and substitutes a generic noun ("your work") rather than
240
+ omitting. Always include FALLBACK.
241
+ - **Vague DO list.** `DO: be specific and human` is not a DO. Spell out
242
+ the verb-led shapes: `DO: use "your work helping X", "your writing on
243
+ Y", "Saw you're pushing into Z"`. The model needs prescriptions, not
244
+ vibes.
245
+ - **DON'T list that only describes obvious failures.** The DON'T clause
246
+ is most valuable when it names the non-obvious failures: product-noun
247
+ substitution, source-citation phrasing, mind-reading from signals,
248
+ generic noun substitutes.
249
+ - **Bracket containing freeform prose.** Keep the bracket's clauses
250
+ labeled (DO / DON'T / FALLBACK). Prose paragraphs are harder for the
251
+ model to scan and easier to ignore.
252
+
253
+ ## Cross-references
254
+
255
+ - `mcp/sellable/skills/generate-messages/SKILL.md` — the skill that
256
+ invokes this token system at message-generation time. Carries the
257
+ hard-invariant rules (no product-noun substitution, omit-fallback as
258
+ default, etc.).
259
+ - `mcp/sellable/skills/create-campaign/references/token-fill-examples.md`
260
+ — example archive showing good/bad fills across many shapes.
261
+ - `mcp/sellable/skills/create-campaign-brief/references/brief-template.md`
262
+ — the brief template that authors should populate with AI-native tokens
263
+ by default.
264
+ - `mcp/sellable/skills/create-campaign-brief/references/phase75-active-runtime-message-pack.md`
265
+ — the runtime gold examples, including the original Sellable message
266
+ with `[PERSONALIZED REASON — ...]` and `[THEIR ICP — ...]`.
@@ -2,6 +2,16 @@
2
2
 
3
3
  Use this file when template-style personalization is in play.
4
4
 
5
+ > **Read `ai-native-tokens.md` first.** The Sellable canonical token style is
6
+ > AI-native: bracketed `[ALL_CAPS_NAME — instructions]` placeholders that
7
+ > name what JOB the line does and carry DO / DON'T / FALLBACK rules inline.
8
+ > The model writes a sentence (or omits) per the inline contract — it does
9
+ > NOT fill a fixed slot. That spec is the source of truth; this file shows
10
+ > good/bad examples across shapes. Old-style `{{field}}` substitutions still
11
+ > apply for atomic values (first name, company name) but are insufficient for
12
+ > personalization sentences — see the Product-Noun Substitution section
13
+ > below for the failure mode they enable.
14
+
5
15
  ## Good Token Fill
6
16
 
7
17
  Good token fill feels like a human wrote the sentence after seeing the
@@ -79,3 +89,86 @@ Revvix / security positioning:
79
89
  - the token makes the sentence longer without making it better
80
90
  - the token sounds like a compliment sandwich
81
91
  - the token could be swapped into any message without changing meaning
92
+
93
+ ## Product-Noun Substitution (HARD INVARIANT — block before fill)
94
+
95
+ When a token sits inside a possessive frame like `your {{X}}`, `at your {{X}}`,
96
+ or `because of your {{X}}`, the filled value must describe something the
97
+ recipient personally **does** — their work, focus, or activity. It must NOT
98
+ describe a product their company **builds or sells**.
99
+
100
+ The grammar test: read `your <filled-value>` out loud. If it reads as if the
101
+ recipient _uses_ or _has_ the thing, but the recipient actually _builds/sells_
102
+ the thing, the fill is wrong → OMIT the entire sentence.
103
+
104
+ **Allowed (buyer activities):**
105
+
106
+ - `monetization research for indie devs`
107
+ - `founder-led-sales experiments`
108
+ - `GTM Engineering writing`
109
+ - `outbound work for AI founders`
110
+ - `pricing-strategy advisory`
111
+ - `B2B SaaS GTM background` (history is fine — it describes the person)
112
+
113
+ **Blocked (product nouns from the prospect's company):**
114
+
115
+ - `Moneyball dashboard for CEOs` ← the prospect BUILDS this; they do not have one of their own
116
+ - `monetization layer for AI builders` ← product description, not buyer activity
117
+ - `AI scoring engine` / `platform for X` / `tool for X` / `API for X` ← any company-output noun
118
+ - `Series A funding round` ← event, not activity
119
+
120
+ Why this matters: the personalization line is supposed to make the buyer feel
121
+ recognized as a person. A product-noun fill instead makes the message sound
122
+ like it was written by an enrichment scraper — and worse, it implies we
123
+ mistook the founder for a customer of their own product.
124
+
125
+ **The omit-fallback is the safe default.** When you cannot produce a
126
+ buyer-activity phrase that survives the grammar test, OMIT the sentence
127
+ entirely. The message must read cleanly without the soft-bridge line. Do NOT
128
+ substitute a generic noun (`your work`, `your stack`, `your team`) — those
129
+ add no relevance and sound mail-merge-y. Aggressive omit > awkward fill.
130
+
131
+ When the brief defines a `{{workflow_context}}` token (or any
132
+ buyer-activity-shaped token), include this rule verbatim in the brief's Token
133
+ Fill Rules section so per-row generation has the constraint in scope.
134
+
135
+ ## AI-Native Tokens (the canonical Sellable pattern)
136
+
137
+ Personalization sentences should be authored as AI-native tokens — bracketed
138
+ instructions, not field substitutions. Field substitutions (`{{first_name}}`,
139
+ `{{company}}`) work for atomic values that drop into a fixed slot. But any
140
+ sentence that requires _judgment_ about what to write should be authored as:
141
+
142
+ ```text
143
+ [ALL_CAPS_NAME — Intent. DO: ... DON'T: ... FALLBACK: omit the line.]
144
+ ```
145
+
146
+ The bracket lives inline in the message template. The model writes a sentence
147
+ (or omits the entire line) following the inline contract. The bracket itself
148
+ is replaced by the rendered sentence — or by nothing.
149
+
150
+ **Why:** every personalization failure documented in this file (mail-merge
151
+ phrasing, product-noun substitution, source-citation, generic noun
152
+ substitutes) shares one root cause: the model was filling a fixed-frame slot
153
+ under rules that lived elsewhere. AI-native tokens fix that by making the
154
+ model COMPOSE a sentence under rules that live INLINE.
155
+
156
+ **The original Sellable gold message (the canonical example)** uses two
157
+ AI-native tokens:
158
+
159
+ ```text
160
+ we're looking for design partners — and [PERSONALIZED REASON — their team
161
+ size, role, or why they're a perfect fit].
162
+
163
+ a) 15-min call — i'll show you how you could book more meetings with
164
+ [THEIR ICP — who they want to reach], and if you like it we launch a pilot
165
+ right there
166
+ ```
167
+
168
+ Notice: `[PERSONALIZED REASON]` and `[THEIR ICP]` name what JOB the line
169
+ does. The model produces a different rendered version per row, but every
170
+ version is grammatically clean because the model is _writing_, not
171
+ _filling_.
172
+
173
+ **Full spec, contract requirements, and brief-authoring guide:** see
174
+ `ai-native-tokens.md` in this same references directory.
@@ -162,6 +162,17 @@ next step, not a section the customer needs to study in detail.
162
162
  Use 0-3 bullets max. If there is nothing special to note yet, say:
163
163
  `None yet — validate from the first lead sample.`
164
164
 
165
+ **Personalization tokens — default to AI-native, not field substitution.**
166
+ Atomic field tokens (`{{first_name}}`, `{{company}}`) are fine for greeting,
167
+ subject, and any sentence that drops a single value into a fixed slot. Any
168
+ personalization SENTENCE (a hook line that anchors the message to the
169
+ prospect) should be authored as an AI-native bracket token in the message
170
+ template — `[INTENT_NAME — Intent. DO: ... DON'T: ... FALLBACK: omit the
171
+ line.]` — not as a `{{field}}` slot inside a fixed frame. The model writes
172
+ the sentence per the inline contract; if it can't satisfy the DOs cleanly,
173
+ it omits the entire line. Aggressive omit > awkward fill. Full spec and
174
+ examples: `mcp/sellable/skills/create-campaign/references/ai-native-tokens.md`.
175
+
165
176
  ## Next Steps
166
177
 
167
178
  Keep this section simple and action-oriented. The customer should understand the