@sellable/mcp 0.1.951-wip.sendercontext.20261001.1 → 0.1.953

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.
@@ -1,8 +1,8 @@
1
1
  import { type AuthStatus } from "./auth.js";
2
2
  import { getCampaignContext } from "./context.js";
3
3
  import { getCampaignFramework } from "./framework.js";
4
- import { type ListSubskillPromptsResponse, type SubskillPromptResponse } from "./prompts.js";
5
4
  import { type CampaignModelQualityResult } from "./model-quality.js";
5
+ import { type ListSubskillPromptsResponse, type SubskillPromptResponse } from "./prompts.js";
6
6
  type BootstrapCreateCampaignInput = {
7
7
  campaignId?: string;
8
8
  flowVersion?: "v1" | "v2";
@@ -2,8 +2,8 @@ import { resetApi } from "../api.js";
2
2
  import { getAuthStatus } from "./auth.js";
3
3
  import { getCampaignContext } from "./context.js";
4
4
  import { getCampaignFramework } from "./framework.js";
5
- import { getSubskillPrompt, listSubskillPrompts, } from "./prompts.js";
6
5
  import { evaluateCampaignModelQuality, getCampaignModelMinimumSummary, } from "./model-quality.js";
6
+ import { getSubskillPrompt, listSubskillPrompts, } from "./prompts.js";
7
7
  function toErrorMessage(error) {
8
8
  return error instanceof Error ? error.message : String(error);
9
9
  }
@@ -307,7 +307,7 @@ export async function bootstrapCreateCampaign(input = {}) {
307
307
  ? resumeDetected
308
308
  ? `Bootstrap complete.${workspaceNotice}${modelNotice} Resume from campaign state and navigation diagnostics first; treat local draft artifacts as debug-only evidence. Then load ${createCampaignSubskill?.name ?? "create-campaign"} instructions with get_subskill_prompt({ subskillName: "${createCampaignSubskill?.name ?? "create-campaign"}" }); if the response has hasMore=true, continue with nextOffset until hasMore=false.`
309
309
  : flowVersion === "v2"
310
- ? `Bootstrap complete.${workspaceNotice}${modelNotice} Load the compact create-campaign-v2 entry prompt once with get_subskill_prompt({ subskillName: "create-campaign-v2" }); Hermes users should start this flow with /sellable-create-campaign. Load flow/reference assets lazily only when that stage needs them. Preserve the pre-intake sequence: confirm auth/workspace status, ask only for the LinkedIn profile URL or handle, normalize handles to a full profile URL, require that profile identity before continuing, run lightweight profile/company lookup, then ask the target, offer, credibility, and prospect-source setup questions. Do not call list_senders or sender discovery during setup; sender availability belongs only to Settings after message approval. Then write the campaign brief, call create_campaign once to mint the watchable shell, surface the returned watch link once before brief approval, and hand off to lead finding without repeating the link.`
310
+ ? `Bootstrap complete.${workspaceNotice}${modelNotice} Load the compact create-campaign-v2 entry prompt once with get_subskill_prompt({ subskillName: "create-campaign-v2" }); Hermes users should start this flow with /sellable-create-campaign. Load flow/reference assets lazily only when that stage needs them. Preserve the pre-intake sequence: confirm auth/workspace status, ask only for the LinkedIn profile URL or handle, normalize handles to a full profile URL, require that profile identity before continuing, run lightweight profile/company lookup, then ask the target, offer, credibility, and prospect-source setup questions. Do not call list_senders or sender discovery during setup; sender availability belongs only to Settings after message approval. Then write the campaign brief, call create_campaign once to mint the watchable shell, surface the returned watch link once before brief approval, and hand off to lead finding without repeating the link. Exception: when you entered from the first-campaign-onboarding skill, its first-campaign rules replace the workspace notice, the slash-command note, the setup questions, and the watch-link handoff described here.`
311
311
  : `Bootstrap complete.${workspaceNotice}${modelNotice} Load ${createCampaignSubskill?.name ?? "create-campaign"} instructions with get_subskill_prompt({ subskillName: "${createCampaignSubskill?.name ?? "create-campaign"}" }); if the response has hasMore=true, continue with nextOffset until hasMore=false. Follow that flow before calling create_campaign.`
312
312
  : "Bootstrap incomplete. Resolve blockingErrors and rerun bootstrap_create_campaign before provider/search/import tools.";
313
313
  // Strip prompt body from createCampaignSubskill — it's loaded via the host
@@ -141,6 +141,7 @@ export interface CreateCampaignInput {
141
141
  offerPositioning?: unknown;
142
142
  campaignBrief?: string;
143
143
  messageGenerationMode?: "template" | "ai-generated";
144
+ messageApproach?: "conversation" | "message_test";
144
145
  currentStep?: string | null;
145
146
  watchNarration?: unknown;
146
147
  leadSourceType?: string | null;
@@ -167,6 +168,7 @@ export interface UpdateCampaignInput {
167
168
  interactionMode?: InteractionMode;
168
169
  enableICPFilters?: boolean | null;
169
170
  useMessagingTemplate?: boolean | null;
171
+ messageApproach?: "conversation" | "message_test";
170
172
  autoApproveFollowups?: boolean;
171
173
  enableFollowUps?: boolean;
172
174
  senderIds?: string[];
@@ -251,6 +253,7 @@ export declare const campaignToolDefinitions: ({
251
253
  offerPositioning?: undefined;
252
254
  campaignBrief?: undefined;
253
255
  messageGenerationMode?: undefined;
256
+ messageApproach?: undefined;
254
257
  currentStep?: undefined;
255
258
  watchNarration?: undefined;
256
259
  leadSourceType?: undefined;
@@ -300,6 +303,7 @@ export declare const campaignToolDefinitions: ({
300
303
  offerPositioning?: undefined;
301
304
  campaignBrief?: undefined;
302
305
  messageGenerationMode?: undefined;
306
+ messageApproach?: undefined;
303
307
  currentStep?: undefined;
304
308
  watchNarration?: undefined;
305
309
  leadSourceType?: undefined;
@@ -351,6 +355,7 @@ export declare const campaignToolDefinitions: ({
351
355
  offerPositioning?: undefined;
352
356
  campaignBrief?: undefined;
353
357
  messageGenerationMode?: undefined;
358
+ messageApproach?: undefined;
354
359
  currentStep?: undefined;
355
360
  watchNarration?: undefined;
356
361
  leadSourceType?: undefined;
@@ -398,6 +403,7 @@ export declare const campaignToolDefinitions: ({
398
403
  offerPositioning?: undefined;
399
404
  campaignBrief?: undefined;
400
405
  messageGenerationMode?: undefined;
406
+ messageApproach?: undefined;
401
407
  currentStep?: undefined;
402
408
  watchNarration?: undefined;
403
409
  leadSourceType?: undefined;
@@ -445,6 +451,7 @@ export declare const campaignToolDefinitions: ({
445
451
  offerPositioning?: undefined;
446
452
  campaignBrief?: undefined;
447
453
  messageGenerationMode?: undefined;
454
+ messageApproach?: undefined;
448
455
  currentStep?: undefined;
449
456
  watchNarration?: undefined;
450
457
  leadSourceType?: undefined;
@@ -535,6 +542,7 @@ export declare const campaignToolDefinitions: ({
535
542
  offerPositioning?: undefined;
536
543
  campaignBrief?: undefined;
537
544
  messageGenerationMode?: undefined;
545
+ messageApproach?: undefined;
538
546
  currentStep?: undefined;
539
547
  watchNarration?: undefined;
540
548
  leadSourceType?: undefined;
@@ -593,6 +601,11 @@ export declare const campaignToolDefinitions: ({
593
601
  enum: string[];
594
602
  description: string;
595
603
  };
604
+ messageApproach: {
605
+ type: string;
606
+ enum: string[];
607
+ description: string;
608
+ };
596
609
  currentStep: {
597
610
  type: string[];
598
611
  description: string;
@@ -619,6 +632,7 @@ export declare const campaignToolDefinitions: ({
619
632
  };
620
633
  safety: {
621
634
  type: string[];
635
+ description: string;
622
636
  };
623
637
  progressLabel: {
624
638
  type: string[];
@@ -859,6 +873,7 @@ export declare const campaignToolDefinitions: ({
859
873
  };
860
874
  safety: {
861
875
  type: string[];
876
+ description: string;
862
877
  };
863
878
  progressLabel: {
864
879
  type: string[];
@@ -969,6 +984,11 @@ export declare const campaignToolDefinitions: ({
969
984
  type: string[];
970
985
  description: string;
971
986
  };
987
+ messageApproach: {
988
+ type: string;
989
+ enum: string[];
990
+ description: string;
991
+ };
972
992
  autoApproveFollowups: {
973
993
  type: string;
974
994
  description: string;
@@ -1036,6 +1056,7 @@ export declare const campaignToolDefinitions: ({
1036
1056
  offerPositioning?: undefined;
1037
1057
  campaignBrief?: undefined;
1038
1058
  messageGenerationMode?: undefined;
1059
+ messageApproach?: undefined;
1039
1060
  currentStep?: undefined;
1040
1061
  watchNarration?: undefined;
1041
1062
  leadSourceType?: undefined;
@@ -1086,6 +1107,7 @@ export declare const campaignToolDefinitions: ({
1086
1107
  senderLinkedinUrl?: undefined;
1087
1108
  offerPositioning?: undefined;
1088
1109
  messageGenerationMode?: undefined;
1110
+ messageApproach?: undefined;
1089
1111
  currentStep?: undefined;
1090
1112
  watchNarration?: undefined;
1091
1113
  leadSourceType?: undefined;
@@ -29,7 +29,10 @@ const WATCH_NARRATION_TOOL_SCHEMA = {
29
29
  visibleState: { type: "string" },
30
30
  agentIntent: { type: "string" },
31
31
  nextAction: { type: ["string", "null"] },
32
- safety: { type: ["string", "null"] },
32
+ safety: {
33
+ type: ["string", "null"],
34
+ description: 'Required whenever currentStep changes: one short line on what will not happen yet (for example "Rules only. No one is contacted."). Optional otherwise.',
35
+ },
33
36
  progressLabel: { type: ["string", "null"] },
34
37
  blockedReason: { type: ["string", "null"] },
35
38
  workerStatuses: {
@@ -444,6 +447,11 @@ export const campaignToolDefinitions = [
444
447
  enum: ["template", "ai-generated"],
445
448
  description: "Defaults to template for autonomous messaging steps. Use ai-generated only when the user explicitly approves freeform AI-generated messages instead of an approved {{token}} template.",
446
449
  },
450
+ messageApproach: {
451
+ type: "string",
452
+ enum: ["conversation", "message_test"],
453
+ description: 'How first messages are written. "conversation": each first message is written for that person by the backend writer and opens a conversation, with no pitch; the campaign must have NO approved message template and no {{token}} copy in the brief. "message_test": one approved message template goes to everyone, for testing copy. Omit to leave the campaign as it is.',
454
+ },
447
455
  currentStep: {
448
456
  type: ["string", "null"],
449
457
  description: "Workflow step ID (headless or UI step ID such as filter-rules)",
@@ -566,6 +574,11 @@ export const campaignToolDefinitions = [
566
574
  type: ["boolean", "null"],
567
575
  description: "Whether to use the messaging template step",
568
576
  },
577
+ messageApproach: {
578
+ type: "string",
579
+ enum: ["conversation", "message_test"],
580
+ description: 'How first messages are written. "conversation": each first message is written for that person by the backend writer and opens a conversation, with no pitch; the campaign must have NO approved message template and no {{token}} copy in the brief. "message_test": one approved message template goes to everyone, for testing copy. Omit to leave the campaign as it is.',
581
+ },
569
582
  autoApproveFollowups: {
570
583
  type: "boolean",
571
584
  description: "Whether drafted campaign follow-ups are automatically approved/sent. This does not draft, approve, or send an individual prospect message during the settings update.",
@@ -1043,7 +1056,10 @@ export async function createCampaign(input) {
1043
1056
  const isAutonomousMessageMint = currentStep === "auto-execute-leads" ||
1044
1057
  currentStep === "validate-sample" ||
1045
1058
  currentStep === "auto-execute-messaging";
1046
- const explicitlyAllowsAiGenerated = input.messageGenerationMode === "ai-generated";
1059
+ // A conversation campaign has no template by design: each first message is
1060
+ // written for the person by the backend writer.
1061
+ const explicitlyAllowsAiGenerated = input.messageGenerationMode === "ai-generated" ||
1062
+ input.messageApproach === "conversation";
1047
1063
  const hasTemplateToken = /\{\{\s*\S[^}]*\}\}/.test(briefContent);
1048
1064
  if (isAutonomousMessageMint &&
1049
1065
  !explicitlyAllowsAiGenerated &&
@@ -12,6 +12,9 @@ export type CampaignOfferNavigation = {
12
12
  useMessagingTemplate?: boolean | null;
13
13
  messagingTemplateId?: string | null;
14
14
  messagingTemplate?: unknown | null;
15
+ messageGenerator?: {
16
+ mode?: string | null;
17
+ } | null;
15
18
  leadSourceType?: string | null;
16
19
  leadSourceProvider?: string | null;
17
20
  selectedLeadListId?: string | null;
@@ -140,6 +140,10 @@ function hasActiveRubrics(campaign) {
140
140
  function hasApprovedMessageTemplate(campaign) {
141
141
  if (campaign.messagingTemplateId || campaign.messagingTemplate)
142
142
  return true;
143
+ // A conversation campaign never has a template; its messages are written
144
+ // per person, so the message approach is already settled.
145
+ if (campaign.messageGenerator?.mode === "CONVERSATION")
146
+ return true;
143
147
  const briefContent = getBriefContent(campaign.campaignBrief);
144
148
  if (!briefContent.trim())
145
149
  return false;
@@ -2334,6 +2334,7 @@ export declare const allTools: ({
2334
2334
  offerPositioning?: undefined;
2335
2335
  campaignBrief?: undefined;
2336
2336
  messageGenerationMode?: undefined;
2337
+ messageApproach?: undefined;
2337
2338
  currentStep?: undefined;
2338
2339
  watchNarration?: undefined;
2339
2340
  leadSourceType?: undefined;
@@ -2383,6 +2384,7 @@ export declare const allTools: ({
2383
2384
  offerPositioning?: undefined;
2384
2385
  campaignBrief?: undefined;
2385
2386
  messageGenerationMode?: undefined;
2387
+ messageApproach?: undefined;
2386
2388
  currentStep?: undefined;
2387
2389
  watchNarration?: undefined;
2388
2390
  leadSourceType?: undefined;
@@ -2434,6 +2436,7 @@ export declare const allTools: ({
2434
2436
  offerPositioning?: undefined;
2435
2437
  campaignBrief?: undefined;
2436
2438
  messageGenerationMode?: undefined;
2439
+ messageApproach?: undefined;
2437
2440
  currentStep?: undefined;
2438
2441
  watchNarration?: undefined;
2439
2442
  leadSourceType?: undefined;
@@ -2481,6 +2484,7 @@ export declare const allTools: ({
2481
2484
  offerPositioning?: undefined;
2482
2485
  campaignBrief?: undefined;
2483
2486
  messageGenerationMode?: undefined;
2487
+ messageApproach?: undefined;
2484
2488
  currentStep?: undefined;
2485
2489
  watchNarration?: undefined;
2486
2490
  leadSourceType?: undefined;
@@ -2528,6 +2532,7 @@ export declare const allTools: ({
2528
2532
  offerPositioning?: undefined;
2529
2533
  campaignBrief?: undefined;
2530
2534
  messageGenerationMode?: undefined;
2535
+ messageApproach?: undefined;
2531
2536
  currentStep?: undefined;
2532
2537
  watchNarration?: undefined;
2533
2538
  leadSourceType?: undefined;
@@ -2618,6 +2623,7 @@ export declare const allTools: ({
2618
2623
  offerPositioning?: undefined;
2619
2624
  campaignBrief?: undefined;
2620
2625
  messageGenerationMode?: undefined;
2626
+ messageApproach?: undefined;
2621
2627
  currentStep?: undefined;
2622
2628
  watchNarration?: undefined;
2623
2629
  leadSourceType?: undefined;
@@ -2676,6 +2682,11 @@ export declare const allTools: ({
2676
2682
  enum: string[];
2677
2683
  description: string;
2678
2684
  };
2685
+ messageApproach: {
2686
+ type: string;
2687
+ enum: string[];
2688
+ description: string;
2689
+ };
2679
2690
  currentStep: {
2680
2691
  type: string[];
2681
2692
  description: string;
@@ -2702,6 +2713,7 @@ export declare const allTools: ({
2702
2713
  };
2703
2714
  safety: {
2704
2715
  type: string[];
2716
+ description: string;
2705
2717
  };
2706
2718
  progressLabel: {
2707
2719
  type: string[];
@@ -2942,6 +2954,7 @@ export declare const allTools: ({
2942
2954
  };
2943
2955
  safety: {
2944
2956
  type: string[];
2957
+ description: string;
2945
2958
  };
2946
2959
  progressLabel: {
2947
2960
  type: string[];
@@ -3052,6 +3065,11 @@ export declare const allTools: ({
3052
3065
  type: string[];
3053
3066
  description: string;
3054
3067
  };
3068
+ messageApproach: {
3069
+ type: string;
3070
+ enum: string[];
3071
+ description: string;
3072
+ };
3055
3073
  autoApproveFollowups: {
3056
3074
  type: string;
3057
3075
  description: string;
@@ -3119,6 +3137,7 @@ export declare const allTools: ({
3119
3137
  offerPositioning?: undefined;
3120
3138
  campaignBrief?: undefined;
3121
3139
  messageGenerationMode?: undefined;
3140
+ messageApproach?: undefined;
3122
3141
  currentStep?: undefined;
3123
3142
  watchNarration?: undefined;
3124
3143
  leadSourceType?: undefined;
@@ -3169,6 +3188,7 @@ export declare const allTools: ({
3169
3188
  senderLinkedinUrl?: undefined;
3170
3189
  offerPositioning?: undefined;
3171
3190
  messageGenerationMode?: undefined;
3191
+ messageApproach?: undefined;
3172
3192
  currentStep?: undefined;
3173
3193
  watchNarration?: undefined;
3174
3194
  leadSourceType?: undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.951-wip.sendercontext.20261001.1",
3
+ "version": "0.1.953",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code, Codex, and Hermes campaign workflows",
6
6
  "main": "dist/index.js",
@@ -53,6 +53,13 @@ The bracket is replaced at generation time by either (a) the rendered
53
53
  sentence, or (b) nothing (omit). The brackets themselves never appear in
54
54
  the final message.
55
55
 
56
+ **When you save the template in the campaign brief, write each bracket on one
57
+ line.** The examples below wrap a bracket across lines so it is readable here.
58
+ The brief validator reads a bracket that spans three or more lines as message
59
+ prose and rejects the save ("3+ consecutive non-empty prose lines with only
60
+ single newlines"). Keep the DO / DON'T / FALLBACK labels; drop the line breaks
61
+ inside the bracket.
62
+
56
63
  ## Required clauses inside an AI-native token
57
64
 
58
65
  Every well-specified AI-native token must include four things:
@@ -80,6 +80,12 @@ approval and campaign setup validation.
80
80
  Use `research-sender`, call `complete_sender_research` before shell creation,
81
81
  and carry proof gaps into the brief.
82
82
 
83
+ ## Message Approach
84
+
85
+ Before `create_campaign`, load `references/message-approach.md` and choose how
86
+ first messages are written: Conversations (each written for the person, no
87
+ template) or Message Lab (one approved template). Pass it as `messageApproach`.
88
+
83
89
  ## Brief Provenance
84
90
 
85
91
  When rendering the first brief, do not add bracketed inline source tags. Start
@@ -308,6 +314,7 @@ Route user copy feedback before `approve-message` back to Message Drafting; pare
308
314
 
309
315
  Load references only when needed:
310
316
 
317
+ - `references/message-approach.md` before `create_campaign`.
311
318
  - `references/watch-link-handoff.md` for watch-link handoff.
312
319
  - `references/watch-guide-narration.md` for watch narration.
313
320
  - `references/lead-validation-preview.md` for legacy/debug preview shapes.
@@ -90,5 +90,8 @@ messages that pass my quality check and launch for you, then keep your sends fil
90
90
  Reply review first if you would rather approve." Save the answer with `customer_program`
91
91
  `save_context` as `launchConsent: { mode: "autopilot" | "review_first", decidedAt }`.
92
92
  A yes, or no objection, is `autopilot`; a wish to approve first is `review_first`.
93
- Never ask again; a later "review first" switches to `review_first`, and "pause" sets the saved work and sending pauses with `set_preferences`. If the
93
+ A reply that answers other questions and says nothing about this offer is not an answer to it:
94
+ save nothing and keep every approval. Never ask again; a later "review first" switches to `review_first`, and "pause" sets the saved work and sending pauses with `set_preferences`. If the
94
95
  customer has not answered when you reach launch, keep the launch confirmation.
96
+ During a new workspace's first campaign (`first-campaign-onboarding`) do not make this offer at all:
97
+ the first launch always needs the customer's explicit yes.
@@ -540,4 +540,6 @@ marketplace actors, wrong function, or no budget path, classify it as
540
540
 
541
541
  After filters are saved, wait for explicit filter approval before Filter Leads.
542
542
  Then wait for message template/token approval before enrichment, scoring, or
543
- Generate Message cells are queued.
543
+ Generate Message cells are queued. A Conversations campaign
544
+ (`references/message-approach.md`) has no template to approve: queue the
545
+ sample once the filters are settled.
@@ -0,0 +1,70 @@
1
+ # Message Approach
2
+
3
+ Every campaign writes its first message one of two ways. Decide which before
4
+ `create_campaign`, pass it as `messageApproach`, and say which you picked in
5
+ one plain line the customer can correct.
6
+
7
+ - **Conversations** (`messageApproach: "conversation"`). Each first message is
8
+ written for that one person by Sellable's writer and opens a conversation.
9
+ It carries no pitch and no ask; the offer comes up once they reply. Choose
10
+ it when replies have an owner: reply drafting is on for the campaign (the
11
+ default), so each reply arrives as a draft to approve, or the workspace's
12
+ Reply Agent answers.
13
+ - **Message Lab** (`messageApproach: "message_test"`). One approved message
14
+ goes to everyone, so the customer can test which copy wins. This is the
15
+ template flow described in the create-campaign-v2 prompt and flow file. Choose it when the
16
+ customer wants to approve exact wording, test copy against copy, or has
17
+ switched reply drafting off.
18
+
19
+ When the customer has not said, use Conversations if replies have an owner and
20
+ Message Lab otherwise. "Same message to everyone", "test my copy" or "I want
21
+ to approve the wording" means Message Lab. "Write each one", "personal",
22
+ "conversational" or "use the reply agent" means Conversations. Change an
23
+ existing campaign with `update_campaign({ campaignId, messageApproach })`.
24
+
25
+ **A Conversations campaign has no template.** The backend writes each message
26
+ only when the campaign carries no approved template, so:
27
+
28
+ - write no `## Approved Message Template` section, no `{{token}}` copy and no
29
+ first-message wording into the brief, and keep pitch wording out of `dos`;
30
+ - put the offer and the proof under headings that say "for replies only";
31
+ - when the first message goes out as a DM after the connection request is
32
+ accepted (the recommended sequence), add this line to the campaign `dos`
33
+ with the sender's first name: "This first message goes out as a LinkedIn DM
34
+ after they accept {sender}'s connection request: thank them for
35
+ connecting.";
36
+ - skip Message Drafting, the message template review and the template
37
+ approval. After the filters are saved, queue enrichment for the sample;
38
+ fit scoring and Generate Message follow on their own;
39
+ - review the generated messages themselves before showing or approving
40
+ them: the person and company match the profile, no pitch, and nothing
41
+ about the person that their profile does not show (a guessed city, a guessed
42
+ role). Leave a message that fails out of the sample, say so in one line,
43
+ and do not approve that row until it is rewritten
44
+ (`queue_campaign_cells` with `columnRole: "generateMessage"`, that row,
45
+ `forceRerun: true`). `get_campaign` reports the saved approach as
46
+ `campaignOffer.messageApproach`.
47
+
48
+ **The writer sounds like the sender only when it knows the sender.** With
49
+ exactly one sender attached to the campaign and that sender's context saved,
50
+ it writes the full message, including a line about the sender. With no sender,
51
+ several senders, or no saved context it writes the short form: a greeting,
52
+ one plain line about the person's work, and a light line about their town. So in a Conversations campaign:
53
+
54
+ 1. Ask for the LinkedIn connection before the sample, not after it.
55
+ 2. Once `list_senders` shows the account, attach it
56
+ (`update_campaign({ campaignId, senderIds: [id] })`), read it with
57
+ `get_sender`, and save what the profile research found with
58
+ `set_sender_context` (pass `get_sender`'s `generationContext.updatedAt` as
59
+ `expectedUpdatedAt`, null on the first save). Use facts from the sender's
60
+ own profile only: what they built and did (`workStories`), plain personal
61
+ facts (`personalNotes`), a few lines in their own words from their posts
62
+ or About (`voiceExamples`), and what they may share or will not say
63
+ (`sourcesAndLimits`).
64
+ 3. Generate or regenerate the sample after that
65
+ (`queue_campaign_cells` with `columnRole: "generateMessage"`, the sample
66
+ rows, `forceRerun: true`).
67
+
68
+ If the customer has not connected an account when the sample is ready, show
69
+ the short-form sample and say the messages gain a line about them once the
70
+ account is connected.
@@ -54,7 +54,8 @@ rerun `import_leads`.
54
54
  only after saved-filter approval; `validate-sample` is the logical loop name,
55
55
  not a visible route)
56
56
  - Approved message template/token rules exist in the campaign brief before any
57
- enrichment or scoring cells are queued
57
+ enrichment or scoring cells are queued (Message Lab only; a Conversations
58
+ campaign has no template)
58
59
  - Confirmed campaign rows from Step 13, with the internal execution slice stored
59
60
  in `WorkflowTable.config.mcp.reviewBatch` (default 15)
60
61
  - Config from `auto-execute.yaml`: `sample.sampleSize`,
@@ -40,7 +40,8 @@ states.
40
40
  sample you are checking, and why that helps this campaign.
41
41
  - At filter choice, do not say the batch is filtering before rubrics are saved.
42
42
  - Avoid internal terms: MCP, tool, currentStep, workflow table, scout, debug.
43
- - Treat `safety` as optional internal context, not a visible disclaimer.
43
+ - Always send a short `safety` line when the step changes; the API rejects a
44
+ step change without one. It is internal context, not a visible disclaimer.
44
45
  - Do not repeat long negative lists like "no leads import, no enrichment, no
45
46
  messages, no sequence, no sending" in routine watch copy. Prefer one concise
46
47
  gate phrase only when it matters: "This only approves me to look for the best
@@ -8,6 +8,13 @@ visibility: internal
8
8
 
9
9
  This is the tail detail extracted from the main create-campaign-v2 SKILL.md to keep the entry prompt slim. The agent loads this on-demand BEFORE entering the auto-execute or validate-sample steps. Follow this verbatim.
10
10
 
11
+ **Conversations campaigns** (`messageApproach: "conversation"`, see
12
+ `create-campaign-v2` `references/message-approach.md`) have no message
13
+ template. Every rule below that requires an approved template, `{{...}}`
14
+ tokens in the brief, Message Drafting, or template approval before queueing
15
+ applies to Message Lab campaigns only. In a Conversations campaign the same
16
+ cascade writes each message once a row passes the fit check.
17
+
11
18
  CampaignOffer state and the watch link are canonical. Disk artifacts are
12
19
  optional debug/UAT diagnostics; normal customer runs should not expose local
13
20
  draft files. Resume, gating, and handoff read campaign state first.
@@ -198,9 +205,8 @@ import milestone.
198
205
  `messaging.critique.enabled`, `handoff.autoStart`,
199
206
  `handoff.orientation`, `retry.sameToolSameError`,
200
207
  `logging.logEveryThresholdTrip`.
201
- 2. Resolve and materialize the approved source after Start Import approval. Load
202
- `references/post-mint-source-materialization.md` when the source is normal
203
- discovery or when a legacy no-shell approval fixture must be replayed. If
208
+ 2. Resolve and materialize the approved source after Start Import approval.
209
+ `references/step-13-import-leads.md` holds the import steps. If
204
210
  CampaignOffer state already attached searches/selections to this campaign,
205
211
  reuse them. If there is no `lead-source-intake.json`, replay the approved
206
212
  provider recipe from `lead-review.md` with `campaignOfferId` before
@@ -381,7 +387,8 @@ the output — not to generate messages manually.
381
387
  **What Step 15 does:**
382
388
 
383
389
  1. Before queueing or waiting on Generate Message cells, confirm the minted
384
- campaign brief still contains `{{...}}` in `## Approved Message Template`.
390
+ campaign brief still contains `{{...}}` in `## Approved Message Template`
391
+ (Message Lab only; a Conversations campaign must have no template).
385
392
  If it does not, fail before the cascade runs. Do not repair after mint; the
386
393
  template must be present during mint so Step 15 never starts in freeform
387
394
  generation mode.
@@ -622,7 +629,6 @@ runs.
622
629
  | ------------------------------------------------ | ------------------------------------------------------------------------- |
623
630
  | `references/approval-gate-framing.md` | Approval gate, before showing the approval packet |
624
631
  | `references/watch-link-handoff.md` | First brief handoff + explicit link recovery only |
625
- | `references/post-mint-source-materialization.md` | Step 13, before importing normal-discovery or legacy campaignless sources |
626
632
  | `references/sample-validation-loop.md` | Step 14, before enriching + scoring the sample |
627
633
  | `references/escalation-ladder.md` | Any tail step that needs to decide retry / revise / escalate |
628
634
  | `references/final-handoff-contract.md` | Step 16, and every Claude greenlight turn |
@@ -48,11 +48,16 @@ reply to either the same way.)
48
48
  A reply in that thread is explicit permission to begin preparation, not
49
49
  permission to send. It needs no mention of this Agent.
50
50
 
51
- - **The reply contains a LinkedIn profile URL.** Before any tool call, send
52
- one short line so the customer is not left looking at silence: "Got it.
53
- Researching you and your company now. I'll be back in a couple of minutes
54
- with a first campaign to review." Then enter **Create-Campaign Handoff** in
55
- the same turn, with that URL as the sender identity.
51
+ - **The reply contains a LinkedIn profile URL or handle.** A single word such
52
+ as `csreyes92` or `@csreyes92` is a handle: read it as
53
+ `https://www.linkedin.com/in/{handle}/`. Send one short line first so
54
+ the customer is not left looking at silence: "Got it. Researching you and
55
+ your company now. I'll be back in a few minutes with a first campaign to
56
+ review." Send it before the Live State Check and before loading any other
57
+ skill or reference; those reads take over a minute. Then run the Live State
58
+ Check and enter **Create-Campaign Handoff** in the same turn, with that URL
59
+ as the sender identity. If the Live State Check finds a campaign already
60
+ launched, say so in the next line and offer to help with that campaign.
56
61
  - **The reply is a yes, or anything else that agrees, with no profile URL.**
57
62
  Ask for it once, in one sentence: "What's your LinkedIn profile URL? I'll
58
63
  use it to research you and your company." Ask nothing else.
@@ -209,18 +214,242 @@ continue in the same conversation; do not ask the user to run another command.
209
214
 
210
215
  1. Call `bootstrap_create_campaign({ flowVersion:"v2", host:<current host> })`.
211
216
  2. Load `get_subskill_prompt({ subskillName:"create-campaign-v2" })` to
212
- `hasMore:false` and load every required asset it names.
217
+ `hasMore:false`, then `core/flow.v2.json`. Load every other reference only
218
+ at the stage that needs it. The customer is waiting in silence, so make
219
+ these reads in as few turns as you can: the Live State Check reads together
220
+ in one turn; then bootstrap, the create-campaign-v2 prompt, the flow file,
221
+ `research-sender` and `list_dnc_entries({ limit:1 })` together in the next.
213
222
  3. Reuse known identity, company, audience, offer, and any explicit DNC decision.
214
- Ask only missing intake in the canonical flow. A public LinkedIn profile URL
215
- for company research is not a connected sending account: the latter belongs
216
- at Settings, not before the draft. Preserve the normal brief/watch, source,
217
- filter, message, sender, sequence, and approval gates. Offer CRM or meeting
218
- integrations only as optional context when helpful.
223
+ A public LinkedIn profile URL for company research is not a connected
224
+ sending account: the latter belongs at Settings, not before the draft.
225
+ The flow's order and state gates still hold: the campaign exists before
226
+ anyone is imported, the source is chosen before the import, sample rows
227
+ exist before filters and messages run, and the template is saved before
228
+ anything is queued. Who decides each one is set by the next section. Offer
229
+ CRM or meeting integrations only as optional context when helpful.
219
230
  4. Reuse an existing draft for the same goal. Never create a duplicate because
220
231
  onboarding resumed in a new session.
221
232
  5. Agreement authorizes campaign creation/preparation only, not launch. Never
222
- call `start_campaign` until the create-campaign workflow presents its final
223
- launch packet and the user explicitly approves Start.
233
+ call `start_campaign` until the customer has seen who sends and how many a
234
+ day, and explicitly says to launch.
235
+
236
+ ### The first campaign in three approvals
237
+
238
+ The welcome promised "a first campaign for you to review". For a new
239
+ workspace's first campaign the customer is asked for three things and nothing
240
+ else: the **direction**, the **sample**, and the **launch**. You decide
241
+ everything in between yourself, using the flow's own hands-off rules
242
+ (`yoloMode.autoSelectsPreLaunchChoices`, except its "brief approval" and
243
+ "generated message review" entries, which are approvals 1 and 2 here), and say
244
+ what you chose in one plain line when you report back. Where the general workflow, a reference file, or a
245
+ tool result says otherwise, this section wins for the first campaign.
246
+
247
+ This is not the standing `launchConsent` autopilot. Do not ask about or save
248
+ `launchConsent` during the first campaign, and never skip one of the three
249
+ approvals. Silence, or a reply that answers something else, is never an
250
+ approval. The daily review offers hands-off sending once the first campaign is
251
+ running.
252
+
253
+ **What you never ask.** Not the four setup questions ("Who should we target
254
+ first?", "What should we pitch?", the credibility question, "How should we
255
+ find prospects?"): giving the profile URL is the customer's instruction to use
256
+ Sellable's researched recommendation and to find prospects for them, which is
257
+ the flow's own skip rule for each question. Not "is this the right company?"
258
+ or "which of your offers should I lead with?": pick the one the profile and
259
+ site lead with and say so in the direction, where they can correct it. Not a
260
+ separate approval for where
261
+ to look, for starting the import, for using filters, for the filter rules, or
262
+ for the message template: those are yours to decide and report.
263
+
264
+ **How you write in Slack.**
265
+
266
+ - Ask in plain words the customer can answer with yes or with the change they
267
+ want. Slack has no question panel: never ask for option letters, and never
268
+ mention a question panel or a config file.
269
+ - Post nothing between "Got it" and the direction: no research progress lines,
270
+ tool names, counts of proof items, or which workspace you are in (this Agent
271
+ is locked to one workspace and the customer knows which).
272
+ - Never print the "WATCH … BUILD THE CAMPAIGN LIVE" block, "Keep this chat
273
+ open", "Brief approved", or any line that tells the customer to run a
274
+ command. Those are written for other hosts.
275
+ - The channel is shared, and the returned `watchUrl` carries a sign-in token.
276
+ Post that URL with only its `token` parameter removed, keeping the rest
277
+ exactly as returned. The customer signed in to Sellable when they set this
278
+ Agent up. This replaces the tool's instruction to print the handoff block
279
+ and its rule against changing the link.
280
+ - Do not paste the full brief into Slack. Save it on the campaign and post the
281
+ five-line summary below.
282
+
283
+ **How the first message is written.** The first campaign uses the
284
+ Conversations approach from the workflow's `references/message-approach.md`:
285
+ create it with `messageApproach: "conversation"`, write no message template,
286
+ and let Sellable's writer write each first message for the person. Use Message
287
+ Lab (`"message_test"`, one approved message to everyone) only when the
288
+ customer asks to send the same message to everyone, to test copy, or to
289
+ approve exact wording; then the template steps of the workflow apply and you
290
+ approve the template yourself as before.
291
+
292
+ **Approval 1: direction.** After research, write the brief, create the
293
+ campaign with it (`create_campaign`), and post one message in this shape. The
294
+ saved brief is the full brief; this message is how the customer approves it,
295
+ so skip the flow's setup receipt, its "Does this brief look right…" question
296
+ and the inline brief.
297
+
298
+ ```text
299
+ Here's the first campaign I'd run for {company}.
300
+
301
+ *Who* {the buyer, one line}
302
+ *Why now* {why these people care right now, one line}
303
+ *Offer* {what you would get to once they reply, and the small ask, one line}
304
+ *Proof* {the proof you would lead with; if you found none, say so and what you would use instead}
305
+ *Where I'll look* {where you will find them first, in plain words}
306
+ *Messages* Written for each person to open a conversation; the offer comes once they reply. Say "same message to everyone" if you'd rather test one message.
307
+
308
+ Full brief: {campaign link}
309
+
310
+ Two things from you:
311
+ 1. Does this look right? Reply yes, or tell me what to change.
312
+ 2. Is there anyone I must never contact: customers, open deals, people who opted out? Paste the list here, or say "no list".
313
+
314
+ Nothing is sent on LinkedIn until you approve the launch.
315
+ ```
316
+
317
+ The first message to a prospect opens a conversation; the message rules
318
+ decide its wording, and they keep the pitch and the ask out of it. So write
319
+ the *Offer* line as what comes once the prospect replies, and do not promise,
320
+ here or in the saved brief, that the first message will mention the post
321
+ someone engaged with or carry the ask.
322
+
323
+ If something you assumed could be stale (for example the profile pitches two
324
+ different things), say which one you picked in the line it affects. Leave out
325
+ the second question, and write "One thing from you:", when
326
+ `list_dnc_entries` already shows a list or the saved program context has
327
+ `dncState` `provided` or `explicit_none`. A yes, or "keep going", with no
328
+ answer to the second question is not a "no list" decision, and it is not a
329
+ reason to stop either: carry on with the list as it is, add to your
330
+ acknowledgement "You can send me a do-not-contact list any time before
331
+ launch.", and ask once more in the launch message. Never hold the work
332
+ waiting for a list. Follow the DNC Contract for a pasted list, and save
333
+ the decision with `customer_program` `save_context` (`dncState:
334
+ "explicit_none"` for "no list", `"provided"` once a list is imported) so a
335
+ later session does not ask again. The answer given here settles the flow's
336
+ do-not-contact gate; do not ask again before the import.
337
+
338
+ **Between 1 and 2.** Once the direction and the do-not-contact answer are in,
339
+ reply first, before any tool call: "On it. I'll find the people, check who
340
+ fits and write the messages. Your sample will be here in about 15 to 20
341
+ minutes. While I work: connect the LinkedIn account that will send,
342
+ https://app.sellable.dev/linkedin-accounts It takes about two minutes, it is
343
+ only used to send what you approve, and once it's connected I write the
344
+ messages in your voice." Then set `interactionMode: "autonomous"` on the campaign with
345
+ `update_campaign` and pass `confirmed: true` where a search or import tool
346
+ asks for it: the direction approval is that confirmation. Do the work without
347
+ asking: pick where to look, find and import the people within the flow's
348
+ caps, use filters and write the rules, and prepare the 15-person sample. In
349
+ Conversations there is no template to write: queue the sample once the rules
350
+ are saved and the messages are written for you.
351
+
352
+ - **Keep the customer posted.** Whenever about five minutes have passed since
353
+ your last message, post one short line in plain words on where you are
354
+ ("Found 370 people who engaged with three posts about this. Checking who
355
+ fits and writing the message now."). No more often than that.
356
+ - **The live builder.** Where the flow's required watch copy tells the
357
+ customer to approve something this section removed, write instead what is
358
+ happening and that nothing is needed from them yet.
359
+ - **Import size.** If `import_leads` says the selected posts cover fewer
360
+ people than the target, select again with the number it says they cover and
361
+ retry once. Do not start a new search for a small gap.
362
+ - **Waits.** A wait tool that stops with `tool_timeout_guard` has only reached
363
+ the host's time limit. Call it again while work is still running.
364
+ - **A sample where nobody fits is not a pause.** Follow the flow's own
365
+ recovery first: a fresh sample from the same list. Stop early only for the
366
+ flow's must-pause reasons: missing data, or a source, filter or message
367
+ that still fails its quality floor after that recovery. Then say what
368
+ failed and what you suggest, as one question.
369
+ - **Let the sample finish.** The flow stops at the first passing message; here
370
+ wait until the 15-person sample has finished scoring and writing, because
371
+ the customer is shown three people and a count.
372
+ - **The sender, as soon as there is one.** Check `list_senders` when the
373
+ import finishes and again before you show the sample. When the customer's
374
+ account is there, attach it to the campaign, save what your research found
375
+ about them as sender context, and write or rewrite the sample after that,
376
+ as `references/message-approach.md` describes. A sample written
377
+ before the account was connected is the short form, with nothing about the
378
+ sender.
379
+ - **Check before you show.** For each message you are about to show, compare
380
+ the company it names with the person's own headline. Leave out any that
381
+ disagree and say that you dropped them.
382
+
383
+ **Approval 2: sample.** When the sample is ready, post real people and the
384
+ real message each would receive:
385
+
386
+ ```text
387
+ Your sample is ready: {the number you show below} real people and the message each would get.
388
+
389
+ {Name} · {title}, {company} · {why they are here, a few words}
390
+ "{the message they would receive}"
391
+
392
+ {two more like this}
393
+
394
+ {passed} of {checked} people I checked fit. {one line on why the rest did not}
395
+ What I chose: {where the people came from and how many are on the list, and the fit rules in a few words}
396
+ Each message is written for that person and has no pitch on purpose. {the offer} comes once they reply.
397
+ {only if no sending account is connected yet: "These are the short version. Connect your LinkedIn account and I'll rewrite them with a line about you: https://app.sellable.dev/linkedin-accounts"}
398
+ Everyone and every message: {campaign link}
399
+
400
+ Reply approve to use these people and messages like these, or tell me what to change.
401
+ Nothing has been sent.
402
+ ```
403
+
404
+ **After "approve".** Reply first: "Approved." plus anything they changed, in
405
+ the same line ("Approved, without Corey."). Mark every person in the sample
406
+ the customer kept as approved in the table, not one or two. A person the
407
+ customer dropped stays unapproved; never approve them later, in bulk or
408
+ otherwise.
409
+
410
+ **Approval 3: launch.** The only thing that can still be missing is the
411
+ sending account. Call `list_senders`.
412
+
413
+ - **No usable sender.** Ask for that one thing: "One thing left: connect the
414
+ LinkedIn account that will send. https://app.sellable.dev/linkedin-accounts
415
+ It takes about two minutes, and it is only used to send what you approve;
416
+ finding and checking people happened outside your account. Tell me when
417
+ it's done." When they say it is done, call `list_senders` again. If nothing
418
+ usable is there yet, say so plainly and give the link again.
419
+ - **One connected sender that is the person you researched.** Use it without
420
+ asking. If the connected account is someone else, or more than one is
421
+ connected, ask which account should send. That is the one extra question
422
+ this section allows.
423
+ - **Get it ready, then ask.** Attach the sender and the recommended sequence
424
+ to the campaign yourself (`update_campaign` with `senderIds`, then
425
+ `attach_recommended_sequence`); this prepares the launch and sends nothing.
426
+ Read the real numbers for that sender (`refill_v3_world_state` for the next
427
+ two days). Then post:
428
+
429
+ ```text
430
+ Ready to launch "{campaign name}".
431
+
432
+ *Sender* {name}
433
+ *First sends* {n} approved messages, {how many a day and which days, from the numbers you read}
434
+ *Replies* I'll post each reply here as a draft for you to approve.
435
+ {only if no do-not-contact answer was ever given: "You haven't sent a do-not-contact list. Paste one now if there is anyone I must not message."}
436
+
437
+ Reply launch to start. You can pause it any time by telling me.
438
+ ```
439
+
440
+ State only numbers you read. If you could not read one, leave that part out
441
+ rather than guess.
442
+ - **Start only on an explicit yes** ("launch", "yes, start", "go"). Then call
443
+ `start_campaign` and report what happened: what is scheduled and for when,
444
+ and that scheduled is not sent yet.
445
+ - **After the launch, one question about the rest.** Only the sample has been
446
+ checked and written. Ask: "{remaining} more people are on the list. Want me
447
+ to keep going through them and approve the messages that pass the same
448
+ checks, so your sends stay filled? Reply yes, or review first." Save the
449
+ answer as the standing `launchConsent` (`customer_program` `save_context`:
450
+ `autopilot` for yes, `review_first` otherwise). This is the first and only
451
+ time that question is asked during onboarding, and only an explicit yes is
452
+ `autopilot`.
224
453
 
225
454
  ## Hard Boundaries
226
455
 
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: research-sender
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."
3
+ description: "Parallel-first sender research protocol. One round of five batched tool calls (fetch_linkedin_profile + fetch_company + 3× WebSearch), or the profile first and then the rest when only a profile URL is known; 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
+ allowed-tools: mcp__sellable__fetch_linkedin_profile mcp__sellable__fetch_company mcp__sellable__complete_sender_research WebSearch WebFetch ToolSearch
6
6
  ---
7
7
 
8
8
  # Research Sender (Parallel-First)
@@ -64,14 +64,24 @@ Concretely, your next assistant message must contain exactly these five
64
64
  `tool_use` blocks, in any order, with no leading or trailing prose:
65
65
 
66
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`.
67
+ 2. `fetch_company({ companyUrl })` — sender's company LinkedIn page (description, industry, employee range, recent posts). `companyUrl` is the LinkedIn company URL. REQUIRED — replaces what `enrich_sender` used to provide for `companySnapshot`.
68
68
  3. `WebSearch({ query: 'site:{companyDomain} ("case study" OR "customer story" OR testimonial OR "success story") "{companyName}"' })` — proof.
69
69
  4. `WebSearch({ query: '"{companyName}" {companyDomain} {currentYear} (funding OR raised OR seed OR series OR hiring OR launch OR "press release")' })` — growth/credibility.
70
70
  5. `WebSearch({ query: '"{companyName}" about product site:{companyDomain}' })` — positioning.
71
71
 
72
+ **When only the profile URL is known** (a new customer gave nothing else), the
73
+ company name, domain and LinkedIn company URL come from the profile, so the
74
+ five calls cannot all go in one message. Call `fetch_linkedin_profile` alone
75
+ first, then emit the other four together in the next message. If the profile
76
+ does not show the company's website, call `fetch_company` next on its own and
77
+ take the website from it, then emit the three searches together. Never guess
78
+ the company or its domain from the handle. This order replaces "exactly these
79
+ five … in a single assistant message" above for this case.
80
+
72
81
  **Self-check before you reply:** if your reply contains fewer than five
73
- `tool_use` blocks (after the optional `ToolSearch` setup turn), you are
74
- violating the protocol. Stop, rewrite the reply with all five.
82
+ `tool_use` blocks (after the optional `ToolSearch` setup turn), or fewer than
83
+ four when the profile was fetched first, you are violating the protocol.
84
+ Stop, rewrite the reply with all of them.
75
85
 
76
86
  **Known limitation (`claude -p` headless mode):** Claude often serializes
77
87
  these calls one-per-turn even with explicit instructions. That's fine — the
@@ -137,6 +147,11 @@ signals, AND the campaign fixture/operator told you proof is required, you
137
147
  MAY issue ONE additional WebFetch on the most promising case-study URL from
138
148
  the proof WebSearch. Cap at one WebFetch. No subagents, no second WebSearch round.
139
149
 
150
+ If the proof and positioning searches returned nothing from the company's own
151
+ site (common when the company name is an ordinary word), you MAY issue that
152
+ ONE WebFetch on the company's homepage instead, to read what it says it does
153
+ and any customers it names. Same cap.
154
+
140
155
  If that still yields nothing, set `proofItemsFound: 0` in
141
156
  `complete_sender_research` and proceed; the brief can ship without proof.
142
157
 
@@ -157,16 +172,21 @@ If no reliable evidence is found, set counts to 0 and include that in `notes`.
157
172
 
158
173
  ## Progress UX
159
174
 
160
- Before issuing the parallel batch:
175
+ In a chat host where every line you write is posted to the customer (Slack),
176
+ write nothing here: the customer was already told research is under way, and
177
+ the next thing they should read is the result. Tool names, "in parallel",
178
+ time estimates and counts of proof items are not for customers.
179
+
180
+ In a terminal host, before issuing the batch:
161
181
 
162
182
  ```
163
- Pulling sender profile + company snapshot + case studies + growth signals + positioning in parallel (~10s)...
183
+ Researching the sender and company…
164
184
  ```
165
185
 
166
186
  After synthesis:
167
187
 
168
188
  ```
169
- Sender research ready — {proofItemsFound} proof items, {credibilitySignalsFound} credibility signals.
189
+ Sender research ready.
170
190
  ```
171
191
 
172
192
  ## What Changed From The Previous Protocol