@sellable/mcp 0.1.158 → 0.1.160

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.
@@ -60,9 +60,14 @@ currentStep: "signal-discovery" })` before sampling so the watched Signal
60
60
  sampled/projected headline-fit rate clears the 10% planning floor. Treat the
61
61
  10% floor as a reject threshold, not as the scrape-count denominator when the
62
62
  actual sample rate is higher.
63
- 8. Select/promote enough right-content posts to plausibly hit the target. If the
64
- warm Signals pool is useful but too small, return the expected warm range and
65
- recommend Sales Nav/Prospeo for scale instead of padding with noisy posts.
63
+ 8. Select/promote enough right-content posts to plausibly hit the target. After
64
+ the sample math is known, treat the promoted sample set and final scrape set
65
+ as separate: recommend the smallest right-content post subset whose
66
+ scrapable/reachable engagers clears the required engager count, with a modest
67
+ buffer when needed. If one 1,200+ engager post clears a ~1,000-engager target,
68
+ recommend scraping that one post, not all 3 sample posts. If the warm Signals
69
+ pool is useful but too small, return the expected warm range and recommend
70
+ Sales Nav/Prospeo for scale instead of padding with noisy posts.
66
71
  9. Return false positives and dead ends explicitly.
67
72
 
68
73
  Return a concise structured result with:
@@ -270,7 +270,7 @@ export async function bootstrapCreateCampaign(input = {}) {
270
270
  ? resumeDetected
271
271
  ? `Bootstrap complete.${workspaceNotice} 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.`
272
272
  : flowVersion === "v2"
273
- ? `Bootstrap complete.${workspaceNotice} Load the compact create-campaign-v2 entry prompt once with get_subskill_prompt({ subskillName: "create-campaign-v2" }); load flow/reference assets lazily only when that stage needs them. Preserve the pre-intake sequence: confirm auth/workspace status, resolve campaign identity, and run lightweight profile/company lookup. Do not call list_senders or sender discovery during setup; sender availability belongs only to Settings after message approval. Then run the short setup/intake, write the campaign brief, call create_campaign once to mint the watchable shell, surface the returned watch link, and hand off to lead finding.`
273
+ ? `Bootstrap complete.${workspaceNotice} Load the compact create-campaign-v2 entry prompt once with get_subskill_prompt({ subskillName: "create-campaign-v2" }); load flow/reference assets lazily only when that stage needs them. Preserve the pre-intake sequence: confirm auth/workspace status, ask for the LinkedIn profile URL first with company website as fallback, run lightweight profile/company lookup, and ask whether the user has a current offer or wants the researched recommendation. Do not call list_senders or sender discovery during setup; sender availability belongs only to Settings after message approval. Then run the short setup/intake, write the campaign brief, call create_campaign once to mint the watchable shell, surface the returned watch link, and hand off to lead finding.`
274
274
  : `Bootstrap complete.${workspaceNotice} 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.`
275
275
  : "Bootstrap incomplete. Resolve blockingErrors and rerun bootstrap_create_campaign before provider/search/import tools.";
276
276
  // Strip prompt body from createCampaignSubskill — it's loaded via the host
@@ -163,6 +163,8 @@ export type SelectPromisingPostsInput = {
163
163
  currentStep?: string | null;
164
164
  selectionMode?: "add" | "replace";
165
165
  mode?: "add" | "replace";
166
+ targetEngagerCount?: number;
167
+ maxPostsToScrape?: number;
166
168
  };
167
169
  export type SetHeadlineICPCriteriaInput = {
168
170
  campaignOfferId: string;
@@ -2000,6 +2002,14 @@ export declare const leadToolDefinitions: ({
2000
2002
  enum: string[];
2001
2003
  description: string;
2002
2004
  };
2005
+ targetEngagerCount: {
2006
+ type: string;
2007
+ description: string;
2008
+ };
2009
+ maxPostsToScrape: {
2010
+ type: string;
2011
+ description: string;
2012
+ };
2003
2013
  currentStep: {
2004
2014
  type: string[];
2005
2015
  description: string;
@@ -2051,8 +2061,6 @@ export declare const leadToolDefinitions: ({
2051
2061
  sourceLeadListId?: undefined;
2052
2062
  targetLeadCount?: undefined;
2053
2063
  mode?: undefined;
2054
- targetEngagerCount?: undefined;
2055
- maxPostsToScrape?: undefined;
2056
2064
  tableId?: undefined;
2057
2065
  campaignName?: undefined;
2058
2066
  keepInSync?: undefined;
@@ -2650,6 +2658,16 @@ export declare function selectPromisingPosts(input: SelectPromisingPostsInput):
2650
2658
  unselectedCount: number;
2651
2659
  criteriaCount: number;
2652
2660
  message: string;
2661
+ recommendedPostCount?: undefined;
2662
+ recommendedTargetEngagerCount?: undefined;
2663
+ } | {
2664
+ success: boolean;
2665
+ selectedCount: number;
2666
+ recommendedPostCount: number;
2667
+ recommendedTargetEngagerCount: number | null;
2668
+ unselectedCount: number;
2669
+ criteriaCount: number;
2670
+ message: string;
2653
2671
  }>;
2654
2672
  export declare function setHeadlineICPCriteria(input: SetHeadlineICPCriteriaInput): Promise<{
2655
2673
  success: boolean;
@@ -745,13 +745,21 @@ function buildSignalDiscoveryResultsWatchNarration(postsReturned) {
745
745
  safety: "Scrape approval is the next gate.",
746
746
  };
747
747
  }
748
- function buildSelectedPostApprovalWatchNarration(selectedPostCount) {
748
+ function buildSelectedPostApprovalWatchNarration(selectedPostCount, recommendedPostCount = selectedPostCount, targetEngagerCount) {
749
+ const selectedPostCopy = `${selectedPostCount.toLocaleString("en-US")} LinkedIn post${selectedPostCount === 1 ? "" : "s"} selected for the source test`;
750
+ const recommendedPostCopy = `${recommendedPostCount.toLocaleString("en-US")} recommended LinkedIn post${recommendedPostCount === 1 ? "" : "s"}`;
751
+ const targetCopy = targetEngagerCount
752
+ ? ` to cover about ${targetEngagerCount.toLocaleString("en-US")} source candidates`
753
+ : "";
754
+ const visibleState = recommendedPostCount < selectedPostCount
755
+ ? `${selectedPostCopy}; the scrape plan only needs ${recommendedPostCopy}${targetCopy}.`
756
+ : `${selectedPostCopy}.`;
749
757
  return {
750
758
  stage: "find-leads",
751
759
  headline: "Approve selected-post scrape",
752
- visibleState: `${selectedPostCount.toLocaleString("en-US")} LinkedIn post${selectedPostCount === 1 ? "" : "s"} selected for the source test.`,
760
+ visibleState,
753
761
  agentIntent: "Codex is asking before scraping this selected engager pool into a source list.",
754
- nextAction: `Approve scraping ${selectedPostCount.toLocaleString("en-US")} selected LinkedIn post${selectedPostCount === 1 ? "" : "s"}`,
762
+ nextAction: `Approve scraping ${recommendedPostCopy}`,
755
763
  safety: "Scrape approval is the next gate.",
756
764
  };
757
765
  }
@@ -762,41 +770,68 @@ function formatApproxInteger(value) {
762
770
  const rounded = value >= 100 ? Math.round(value / 10) * 10 : Math.round(value);
763
771
  return `~${rounded.toLocaleString("en-US")}`;
764
772
  }
765
- function buildSignalDiscoverySourceRecommendation({ selectedPosts, }) {
773
+ function normalizeEngagementCount(value) {
774
+ return typeof value === "number" && Number.isFinite(value) && value > 0
775
+ ? value
776
+ : 0;
777
+ }
778
+ function buildSignalDiscoverySourceRecommendation({ selectedPosts, targetEngagerCount, maxPostsToScrape, }) {
766
779
  const { targetGoodFitLeads, defaultFitRate, minPlanningFitRate, sourceCandidateTarget, reviewBatchSize, } = getSignalDiscoverySourcePlanDefaults();
767
- const selectedCount = selectedPosts.length;
768
- const totalEngagement = selectedPosts.reduce((sum, post) => sum + (post.likes ?? 0) + (post.comments ?? 0), 0);
769
- const tableRows = selectedPosts
780
+ const effectiveTargetEngagerCount = normalizePositiveInteger(targetEngagerCount) ?? sourceCandidateTarget;
781
+ const normalizedSelectedPosts = selectedPosts.map((post) => ({
782
+ ...post,
783
+ likes: normalizeEngagementCount(post.likes),
784
+ comments: normalizeEngagementCount(post.comments),
785
+ }));
786
+ const scrapePlan = selectSignalPostsForImport(normalizedSelectedPosts, {
787
+ targetEngagerCount: effectiveTargetEngagerCount,
788
+ maxPostsToScrape: maxPostsToScrape ?? undefined,
789
+ });
790
+ const recommendedPosts = scrapePlan.posts;
791
+ const selectedCount = normalizedSelectedPosts.length;
792
+ const recommendedCount = recommendedPosts.length;
793
+ const totalVisibleEngagement = normalizedSelectedPosts.reduce((sum, post) => sum + post.likes + post.comments, 0);
794
+ const recommendedVisibleEngagement = recommendedPosts.reduce((sum, post) => sum + post.likes + post.comments, 0);
795
+ const tableRows = recommendedPosts
770
796
  .map((post) => {
771
- const engagement = (post.likes ?? 0) + (post.comments ?? 0);
772
- return `| ${escapeMarkdownTableCell(post.authorName)} | ${escapeMarkdownTableCell(post.reason)} | ${formatApproxInteger(engagement)} |`;
797
+ const engagement = post.likes + post.comments;
798
+ return `| ${escapeMarkdownTableCell(post.authorName)} | ${escapeMarkdownTableCell(post.reason)} | ${formatApproxInteger(engagement)} | ${formatApproxInteger(estimateScrapableSignalEngagers(post))} |`;
773
799
  })
774
800
  .join("\n");
775
- const estimatedGoodFit = totalEngagement * defaultFitRate;
776
- return `## Source Recommendation
801
+ const fitRateForEstimate = targetEngagerCount && effectiveTargetEngagerCount > 0
802
+ ? Math.min(1, targetGoodFitLeads / effectiveTargetEngagerCount)
803
+ : defaultFitRate;
804
+ const estimatedGoodFit = scrapePlan.estimatedEngagers * fitRateForEstimate;
805
+ const fitRateLabel = targetEngagerCount && effectiveTargetEngagerCount > 0
806
+ ? "approved source math"
807
+ : `${Math.round(defaultFitRate * 100)}% working assumption`;
808
+ const selectedPoolCopy = recommendedCount < selectedCount
809
+ ? `**Promoted sample pool:** ${selectedCount.toLocaleString("en-US")} selected posts, ${formatApproxInteger(totalVisibleEngagement)} visible / ${formatApproxInteger(scrapePlan.availableEngagers)} scrapable engagers<br>\n**Recommended scrape set:** ${recommendedCount.toLocaleString("en-US")} post${recommendedCount === 1 ? "" : "s"}, ${formatApproxInteger(recommendedVisibleEngagement)} visible / ${formatApproxInteger(scrapePlan.estimatedEngagers)} scrapable engagers<br>`
810
+ : `**Total visible pool:** ${formatApproxInteger(totalVisibleEngagement)} engagers<br>\n**Scrape-capacity pool:** ${formatApproxInteger(scrapePlan.estimatedEngagers)} scrapable engagers after endpoint caps<br>`;
811
+ const message = `## Source Recommendation
777
812
 
778
813
  Use LinkedIn engagement first.
779
814
 
780
815
  **Goal:** ~${targetGoodFitLeads.toLocaleString("en-US")} headline-fit prospects from relevant LinkedIn engagement<br>
781
816
  **Working assumption:** ~${Math.round(defaultFitRate * 100)}% of raw post engagers pass headline filtering unless a real sample supports a different rate<br>
782
- **Engagers needed:** ~${sourceCandidateTarget.toLocaleString("en-US")} raw engagers<br>
817
+ **Engagers needed:** ~${effectiveTargetEngagerCount.toLocaleString("en-US")} raw engagers<br>
783
818
  **Planning floor:** continue with LinkedIn engagement only when sampled/projected headline-fit rate is at least ${Math.round(minPlanningFitRate * 100)}%; below that, switch to Sales Nav recent activity<br>
784
819
  **Review checkpoint:** copy the confirmed source list into the campaign, then process the first ${reviewBatchSize.toLocaleString("en-US")} leads for fit and message review before scaling
785
820
 
786
- ### Selected posts
821
+ ### Recommended scrape set
787
822
 
788
- | Post | Why it fits | Visible engagement |
789
- |---|---|---:|
790
- ${tableRows || "| Selected posts | Campaign-matched public engagement | - |"}
823
+ | Post | Why it fits | Visible engagement | Scrape capacity |
824
+ |---|---|---:|---:|
825
+ ${tableRows || "| Recommended posts | Campaign-matched public engagement | - | - |"}
791
826
 
792
- **Total visible pool:** ${formatApproxInteger(totalEngagement)} engagers<br>
793
- **Estimated headline-fit pool at ${Math.round(defaultFitRate * 100)}%:** ${formatApproxInteger(estimatedGoodFit)} prospects before enrichment and deeper fit review
827
+ ${selectedPoolCopy}
828
+ **Estimated headline-fit pool from ${fitRateLabel}:** ${formatApproxInteger(estimatedGoodFit)} prospects before enrichment and deeper fit review
794
829
 
795
830
  ### Recommendation
796
831
 
797
- Approve scraping these ${selectedCount} posts.
832
+ Approve scraping these ${recommendedCount} recommended post${recommendedCount === 1 ? "" : "s"}.
798
833
 
799
- This gives enough volume to target ~${targetGoodFitLeads.toLocaleString("en-US")} headline-fit prospects, while keeping the source tied to people already engaging with the campaign's strongest public buying signals.
834
+ This gives enough volume to target ~${targetGoodFitLeads.toLocaleString("en-US")} headline-fit prospects, while keeping the scrape limited to the smallest selected right-content post set that covers the source-candidate target.
800
835
 
801
836
  **First pass:** build the source list, copy it into the campaign, then use the first ${reviewBatchSize.toLocaleString("en-US")} campaign rows as the internal setup slice for filters and messages before scaling.
802
837
 
@@ -804,7 +839,13 @@ This gives enough volume to target ~${targetGoodFitLeads.toLocaleString("en-US")
804
839
 
805
840
  Approval card should say:
806
841
 
807
- **Approve scraping ${selectedCount} selected LinkedIn post${selectedCount === 1 ? "" : "s"}?**`;
842
+ **Approve scraping ${recommendedCount} recommended LinkedIn post${recommendedCount === 1 ? "" : "s"}?**`;
843
+ return {
844
+ message,
845
+ recommendedPostCount: recommendedCount,
846
+ estimatedEngagers: scrapePlan.estimatedEngagers,
847
+ targetEngagerCount: effectiveTargetEngagerCount,
848
+ };
808
849
  }
809
850
  function normalizeImportProvider(provider) {
810
851
  if (provider === "apollo-ai" || provider === "apollo")
@@ -1480,7 +1521,7 @@ export const leadToolDefinitions = [
1480
1521
  },
1481
1522
  {
1482
1523
  name: "select_promising_posts",
1483
- description: "Select the most promising LinkedIn posts for lead scraping AND provide headline ICP criteria. Use the selectionTarget returned by search_signals (default 3). In campaign-attached sampling, call select_promising_posts first to promote the sampled posts into the watched UI before fetch_post_engagers.",
1524
+ description: "Select the most promising LinkedIn posts for lead scraping AND provide headline ICP criteria. Use the selectionTarget returned by search_signals (default 3) for the sampling/promoted set. After sample math exists, pass targetEngagerCount so the approval recommendation uses the smallest right-content post subset that covers the source-candidate target instead of scraping every promoted sample post.",
1484
1525
  inputSchema: {
1485
1526
  type: "object",
1486
1527
  properties: {
@@ -1520,6 +1561,14 @@ export const leadToolDefinitions = [
1520
1561
  enum: ["add", "replace"],
1521
1562
  description: 'How to apply selections. "add" keeps existing selections; "replace" clears previous selections not included in this call.',
1522
1563
  },
1564
+ targetEngagerCount: {
1565
+ type: "number",
1566
+ description: "Optional source-candidate target from sample math. When provided, the recommendation ranks selected posts by scrapable engagement and asks approval for only enough posts to cover this target. If omitted, uses the default Signal Discovery source target.",
1567
+ },
1568
+ maxPostsToScrape: {
1569
+ type: "number",
1570
+ description: "Optional hard cap for the recommended scrape set after ranking selected posts by scrapable engagement. Values above the backend hard cap are clamped.",
1571
+ },
1523
1572
  currentStep: {
1524
1573
  type: ["string", "null"],
1525
1574
  description: "Headless workflow step ID",
@@ -3016,7 +3065,7 @@ export function getProviderPrompt(input) {
3016
3065
  }
3017
3066
  export async function selectPromisingPosts(input) {
3018
3067
  const api = getApi();
3019
- const { campaignOfferId, selections, headlineICPCriteria, selectionMode, mode, } = input;
3068
+ const { campaignOfferId, selections, headlineICPCriteria, selectionMode, mode, targetEngagerCount, maxPostsToScrape, } = input;
3020
3069
  const effectiveMode = selectionMode ?? mode ?? "add";
3021
3070
  if (selections.length > MAX_SIGNAL_DISCOVERY_POSTS) {
3022
3071
  return {
@@ -3053,11 +3102,9 @@ export async function selectPromisingPosts(input) {
3053
3102
  message: "No Signal Discovery posts were selected for this campaign. Do not import yet. Re-run a campaign-scoped search_signals call, use recommendedPostIds from the current campaign search state, or ask the user to promote posts in the UI before retrying select_promising_posts.",
3054
3103
  };
3055
3104
  }
3056
- await api.put(`/api/v2/campaign-offers/${campaignOfferId}`, {
3057
- currentStep: "signal-discovery",
3058
- watchNarration: buildSelectedPostApprovalWatchNarration(selectionResult.selectedCount),
3059
- });
3060
3105
  let sourceRecommendation = "";
3106
+ let recommendedPostCount = selectionResult.selectedCount;
3107
+ let recommendationTargetEngagerCount = null;
3061
3108
  try {
3062
3109
  const tabsResponse = await api.get(`/api/v3/campaigns/${campaignOfferId}/signal-discovery/tabs`);
3063
3110
  const reasonsByPostId = new Map(selections.map((selection) => [selection.postId, selection.reason]));
@@ -3086,9 +3133,17 @@ export async function selectPromisingPosts(input) {
3086
3133
  });
3087
3134
  }
3088
3135
  }
3089
- sourceRecommendation = buildSignalDiscoverySourceRecommendation({
3136
+ if (selectedByUrl.size === 0) {
3137
+ throw new Error("No selected Signal Discovery posts found in tabs");
3138
+ }
3139
+ const recommendation = buildSignalDiscoverySourceRecommendation({
3090
3140
  selectedPosts: Array.from(selectedByUrl.values()),
3141
+ targetEngagerCount,
3142
+ maxPostsToScrape,
3091
3143
  });
3144
+ sourceRecommendation = recommendation.message;
3145
+ recommendedPostCount = recommendation.recommendedPostCount;
3146
+ recommendationTargetEngagerCount = recommendation.targetEngagerCount;
3092
3147
  }
3093
3148
  catch {
3094
3149
  const { reviewBatchSize } = getSignalDiscoverySourcePlanDefaults();
@@ -3096,22 +3151,28 @@ export async function selectPromisingPosts(input) {
3096
3151
 
3097
3152
  Use LinkedIn engagement first.
3098
3153
 
3099
- **Recommendation:** approve scraping the ${selectionResult.selectedCount} selected LinkedIn post${selectionResult.selectedCount === 1 ? "" : "s"}.
3154
+ **Recommendation:** approve scraping ${selectionResult.selectedCount} recommended LinkedIn post${selectionResult.selectedCount === 1 ? "" : "s"}.
3100
3155
 
3101
3156
  **First pass:** build the source list, copy it into the campaign, then process only the first ${reviewBatchSize.toLocaleString("en-US")} leads so we can inspect quality before scaling.
3102
3157
 
3103
3158
  Approval card should say:
3104
3159
 
3105
- **Approve scraping ${selectionResult.selectedCount} selected LinkedIn post${selectionResult.selectedCount === 1 ? "" : "s"}?**`;
3160
+ **Approve scraping ${selectionResult.selectedCount} recommended LinkedIn post${selectionResult.selectedCount === 1 ? "" : "s"}?**`;
3106
3161
  }
3162
+ await api.put(`/api/v2/campaign-offers/${campaignOfferId}`, {
3163
+ currentStep: "signal-discovery",
3164
+ watchNarration: buildSelectedPostApprovalWatchNarration(selectionResult.selectedCount, recommendedPostCount, recommendationTargetEngagerCount),
3165
+ });
3107
3166
  return {
3108
3167
  success: true,
3109
3168
  selectedCount: selectionResult.selectedCount,
3169
+ recommendedPostCount,
3170
+ recommendedTargetEngagerCount: recommendationTargetEngagerCount,
3110
3171
  unselectedCount: selectionResult.unselectedCount,
3111
3172
  criteriaCount: selectionResult.criteriaCount,
3112
3173
  message: `${sourceRecommendation}
3113
3174
 
3114
- Selected ${selectionResult.selectedCount} posts with ${selectionResult.criteriaCount} ICP criteria persisted. Ask the user to approve this specific scraping action once; after approval, call import_leads immediately and do not repeat this source card.`,
3175
+ Selected ${selectionResult.selectedCount} posts with ${selectionResult.criteriaCount} ICP criteria persisted. Ask the user to approve the recommended scrape set once; after approval, call import_leads with targetEngagerCount ${recommendationTargetEngagerCount ?? "from the approved source math"} immediately and do not repeat this source card.`,
3115
3176
  };
3116
3177
  }
3117
3178
  export async function setHeadlineICPCriteria(input) {
@@ -3150,6 +3150,14 @@ export declare const allTools: ({
3150
3150
  enum: string[];
3151
3151
  description: string;
3152
3152
  };
3153
+ targetEngagerCount: {
3154
+ type: string;
3155
+ description: string;
3156
+ };
3157
+ maxPostsToScrape: {
3158
+ type: string;
3159
+ description: string;
3160
+ };
3153
3161
  currentStep: {
3154
3162
  type: string[];
3155
3163
  description: string;
@@ -3201,8 +3209,6 @@ export declare const allTools: ({
3201
3209
  sourceLeadListId?: undefined;
3202
3210
  targetLeadCount?: undefined;
3203
3211
  mode?: undefined;
3204
- targetEngagerCount?: undefined;
3205
- maxPostsToScrape?: undefined;
3206
3212
  tableId?: undefined;
3207
3213
  campaignName?: undefined;
3208
3214
  keepInSync?: undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.158",
3
+ "version": "0.1.160",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -86,8 +86,8 @@ instruction loading, file lookup, plugin cache versions, missing linked files,
86
86
  or tool discovery. Start in product language:
87
87
 
88
88
  ```text
89
- I’ll help you launch this as a Sellable campaign. First I’ll resolve the
90
- client/company this campaign is for, then I’ll turn that into a campaign brief
89
+ I’ll help you launch this as a Sellable campaign. First I’ll research the
90
+ person/company this campaign is for, then I’ll turn that into a campaign brief
91
91
  before we move into lead sourcing.
92
92
  ```
93
93
 
@@ -163,11 +163,13 @@ precision, and referral paths, but it does not provide hiring-by-role filters;
163
163
  say that distinction plainly in the source-plan gate.
164
164
 
165
165
  After scouting, ask for a second approval on the concrete source action. For
166
- LinkedIn engagement (`signal-discovery` internally), name how many selected
167
- posts will be scraped and the target engager/source-candidate volume. For
168
- Sales Nav or Prospeo, name the specific approved import lane and source lead
169
- count. Keep the internal 15-row campaign-table execution slice separate from
170
- source sampling.
166
+ LinkedIn engagement (`signal-discovery` internally), name how many
167
+ recommended posts will be scraped and the target engager/source-candidate
168
+ volume. N must be the smallest right-content post set that clears the source
169
+ target, not the default 3 promoted sample posts. For Sales Nav or Prospeo,
170
+ name the specific approved import lane and source lead count. Keep the
171
+ internal 15-row campaign-table execution slice separate from source
172
+ sampling.
171
173
 
172
174
  Do not call `import_leads` or `confirm_lead_list` until this second approval is
173
175
  granted.
@@ -185,8 +187,8 @@ granted.
185
187
  the campaign table and return the initial campaign-table execution slice rows.
186
188
 
187
189
  For LinkedIn engagement, the customer-facing approval card must use the exact
188
- action shape "Approve scraping N selected LinkedIn posts?" and the chat summary
189
- should be a compact `## Source Recommendation` block with:
190
+ action shape "Approve scraping N recommended LinkedIn posts?" and the chat
191
+ summary should be a compact `## Source Recommendation` block with:
190
192
 
191
193
  - goal: about 300 headline-fit prospects from relevant LinkedIn engagement
192
194
  - source-candidate plan: use sample math first: target headline-fit prospects
@@ -396,10 +398,10 @@ customer-facing progress copy.
396
398
 
397
399
  Do not treat the active Sellable workspace as the campaign subject. The
398
400
  workspace only tells you where the campaign will be saved. Before buyer, CTA,
399
- proof, or source questions, identify the campaign identity: the person/profile
400
- or company this campaign is for, plus enough company/product context to build
401
- the brief. This is only the client-prospect/bootstrap identity for
402
- `clientProspectId` or `senderLinkedinUrl`; it is not a connected-sender check.
401
+ proof, or source questions, identify the person/profile or company this
402
+ campaign is for, plus enough current company/product context to build the
403
+ brief. This client/company lookup feeds `clientProspectId` or
404
+ `senderLinkedinUrl`; it is not a connected-sender check.
403
405
 
404
406
  Do not call `mcp__sellable__list_senders`, `mcp__sellable__get_sender`, or
405
407
  surface connected/missing sender state during setup, brief, source, filter, or
@@ -423,19 +425,20 @@ first:
423
425
  only if a URL/domain is also available.
424
426
 
425
427
  Then summarize what you found in one or two lines and ask the user to confirm
426
- the campaign identity/focus before continuing. Do not mention connected sender
427
- availability in this confirmation.
428
+ the current company/focus before continuing, especially if public website data
429
+ may be stale. Do not mention connected sender availability in this confirmation.
428
430
 
429
431
  If the user did not provide the launch identity, ask in normal chat for the
430
- LinkedIn profile or company website to use as the campaign identity. Do not ask
432
+ LinkedIn profile URL first, with the company website as the fallback. Do not ask
431
433
  them to choose an input type with the structured question tool:
432
434
 
433
435
  ```text
434
436
  I’m ready to build this in {workspace}.
435
437
 
436
- First, paste the LinkedIn profile or company website for the client/company this
437
- campaign is for. I’ll use that to resolve the campaign identity before we pick
438
- the target, offer, proof, and lead source.
438
+ What is your LinkedIn profile URL? If you do not have it handy, send your
439
+ company website instead. I’ll research the person and company from that, then
440
+ ask you to correct anything stale before we pick the target, offer, proof, and
441
+ lead source.
439
442
  ```
440
443
 
441
444
  After the user pastes a URL/domain, do the lightweight lookup. For a LinkedIn
@@ -445,10 +448,19 @@ most recent company from the profile. For a company website, call
445
448
  LinkedIn profile URL is available, retain it as `senderLinkedinUrl` for
446
449
  `create_campaign`; if a `clientProspectId` is available, pass that instead.
447
450
 
448
- After the user confirms the campaign identity, run one lightweight company
449
- lookup if it has not already run, then ask the campaign setup questions. The
450
- setup questions should use the confirmed company context so they do not feel
451
- generic.
451
+ After the user confirms the company/focus, run one lightweight company lookup
452
+ if it has not already run, then ask an offer-readiness question before inferred
453
+ strategy hardens:
454
+
455
+ ```text
456
+ Do you already know the offer for this campaign, should I use the researched
457
+ recommendation, or should we shape the offer together? If the website is stale,
458
+ tell me what is current before I build the brief.
459
+ ```
460
+
461
+ The setup questions should use the confirmed company context so they do not feel
462
+ generic. When you present a recommendation, introduce it as based on the
463
+ research you just did and keep it editable.
452
464
 
453
465
  ### Sufficient Intake Bypass
454
466
 
@@ -476,9 +488,9 @@ If the invocation or any later user message explicitly asks for "yolo mode",
476
488
  me", "use best estimates", or "just run it", enable YOLO mode for the rest of
477
489
  the run. Treat YOLO as `interactionMode: "autonomous"` plus an intake policy:
478
490
 
479
- - If campaign identity is missing, ask only for the LinkedIn profile or company
480
- website in normal chat; do not ask buyer, offer, proof, source, or filter setup
481
- questions before that.
491
+ - If the campaign subject is missing, ask only for the LinkedIn profile URL
492
+ first, with company website as the fallback, in normal chat; do not ask buyer,
493
+ offer, proof, source, or filter setup questions before that.
482
494
  - Treat any freeform directions already provided, or added later by the user, as
483
495
  operator directions for the rest of the run. If directions conflict, the newest
484
496
  user direction wins.
@@ -500,8 +512,9 @@ Before the identity gate, use this customer-facing shape:
500
512
  ```text
501
513
  I’m ready to build the campaign in {workspace}.
502
514
 
503
- First I’ll resolve the client/company this campaign is for. I’ll use that
504
- context to choose the target, offer, proof, and lead source.
515
+ First I’ll research the person/company this campaign is for. I’ll use that
516
+ context to recommend a target, offer, proof, and lead source, then you can
517
+ correct anything stale before I build from it.
505
518
 
506
519
  Then I’ll turn that into a campaign brief for you to approve before any leads
507
520
  are sourced.
@@ -636,9 +649,9 @@ updates.
636
649
  ```text
637
650
  You're in — {activeWorkspaceName} workspace, ready to roll.
638
651
 
639
- Now — paste the LinkedIn profile or company website for the client/company this campaign is for. I’ll use that to resolve the campaign identity before we pick the target, offer, proof, and lead source.
652
+ Now — what is your LinkedIn profile URL? If you do not have it handy, send your company website instead. I’ll research the person and company from that, then ask you to correct anything stale before we pick the target, offer, proof, and lead source.
640
653
 
641
- e.g. https://example.com or https://www.linkedin.com/in/client-handle
654
+ e.g. https://www.linkedin.com/in/client-handle or https://example.com
642
655
  ```
643
656
 
644
657
  - If `isReturningUser === false`, prepend ONE line confirming the new
@@ -647,9 +660,9 @@ updates.
647
660
  ```text
648
661
  You're set up — your {activeWorkspaceName} workspace is ready.
649
662
 
650
- Now — paste the LinkedIn profile or company website for the client/company this campaign is for. I’ll use that to resolve the campaign identity before we pick the target, offer, proof, and lead source.
663
+ Now — what is your LinkedIn profile URL? If you do not have it handy, send your company website instead. I’ll research the person and company from that, then ask you to correct anything stale before we pick the target, offer, proof, and lead source.
651
664
 
652
- e.g. https://example.com or https://www.linkedin.com/in/client-handle
665
+ e.g. https://www.linkedin.com/in/client-handle or https://example.com
653
666
  ```
654
667
 
655
668
  No other lines. No "all set", no "signed in", no other acknowledgement.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: create-campaign-v2
3
- description: Execute the compact JSON-gated shell-first campaign flow: resolve campaign identity, create a watchable CampaignOffer shell with the brief, attach and approve the source, copy the confirmed source list into the campaign, internally process the first campaign-table execution slice, persist rubrics and approved message template, validate the first passing generated message, then hand off to Settings, sequence, and start.
3
+ description: Execute the compact JSON-gated shell-first campaign flow from client/offer resolution through source approval, sample/message review, Settings, sequence, and explicit start.
4
4
  visibility: internal
5
5
  ---
6
6
 
@@ -28,15 +28,14 @@ handoff read campaign state first: `campaignId`, `watchUrl`, `campaignBrief`,
28
28
  `currentStep`, source/search association, `selectedLeadListId`,
29
29
  `workflowTableId`, `leadScoringRubrics`, `approvedMessageTemplate`,
30
30
  `senderIds`, `sequenceTemplate`, and running state. Local draft files are
31
- legacy debug/UAT diagnostics only. In normal customer runs, do not create,
32
- read, link, or surface local draft artifacts unless the user explicitly asks
33
- for debug output.
31
+ legacy debug/UAT only; do not create, link, or surface them in normal runs
32
+ unless the user asks for debug output.
34
33
 
35
34
  ## Normal Flow
36
35
 
37
36
  1. Bootstrap and tell the user the active Sellable workspace.
38
- 2. Resolve campaign identity before strategy questions.
39
- 3. Research the client/company enough to draft a concrete brief.
37
+ 2. Resolve the client/company before strategy questions.
38
+ 3. Research the client/company and current offer enough to draft a concrete brief.
40
39
  4. Create the watchable campaign shell with `create_campaign` and the v1 brief.
41
40
  5. Surface the direct watch link.
42
41
  6. Choose and approve the lead source.
@@ -61,18 +60,22 @@ flows and the separate `create-campaign-v2-validation` subskill.
61
60
  ## Identity-First Campaign Setup
62
61
 
63
62
  Do not treat the active Sellable workspace as the campaign subject. Resolve the
64
- client/company first, then draft the buyer, offer, proof, and source plan.
63
+ client/company first, confirm whether the user already has a current offer, then
64
+ draft the buyer, offer, proof, and source plan.
65
65
 
66
66
  First visible request when no identity is known:
67
67
 
68
68
  ```text
69
- First, paste the LinkedIn profile or company website for the client/company this campaign is for.
69
+ What is your LinkedIn profile URL? If you do not have it handy, send your company website instead.
70
70
  ```
71
71
 
72
72
  After the user pastes a URL/domain, retain it as `senderLinkedinUrl` or the
73
73
  resolved `clientProspectId` input for `create_campaign`. Use one lightweight
74
- profile/company lookup before strategy questions. If multiple product lines or
75
- offers are plausible, ask one campaign-focus choice before buyer/offer/proof.
74
+ profile/company lookup before strategy questions, then say that public research
75
+ may be stale and invite the user to correct it. Ask whether they already know
76
+ the offer for this campaign, want Sellable's researched recommendation, or want
77
+ to shape one together before buyer/offer/proof hardens. If multiple product
78
+ lines or offers are plausible, ask one campaign-focus choice before the brief.
76
79
 
77
80
  Do not call `list_senders`, do not infer the campaign from connected senders,
78
81
  and do not show a sender picker during setup. Sender availability belongs only
@@ -83,13 +86,33 @@ Use `research-sender` for concise identity/proof research and call
83
86
  use the available Sellable profile/company/post tools and carry explicit proof
84
87
  gaps into the brief.
85
88
 
89
+ ## Brief Provenance
90
+
91
+ When rendering the first brief, label the major strategic choices with compact
92
+ source tags where the basis is known: `[from you]`, `[from LinkedIn]`,
93
+ `[from website]`, `[from case study]`, or `[Sellable recommendation]`. Keep the
94
+ tags lightweight; do not turn the brief into a research dump.
95
+
96
+ Before asking for approval, say:
97
+
98
+ ```text
99
+ This is based on what I found. If your site or LinkedIn is stale, tell me what
100
+ to update before I build the lead list.
101
+ ```
102
+
103
+ Ask: "Do you agree with this researched campaign direction?" The early brief
104
+ must not include quoted first-message copy. If a message-shape hint is useful,
105
+ keep it as non-final bullets and say: "This is not the message yet; I will write
106
+ real examples after we find and filter leads."
107
+
86
108
  ## YOLO Mode
87
109
 
88
110
  Enable YOLO mode when the user asks for yolo/autopilot, passes `--yolo` or
89
111
  `mode=yolo`, or says to use best guesses/estimates and answer for them. Ask only
90
- for the LinkedIn profile or company website if identity is missing. After the
91
- identity lookup, infer buyer, offer/CTA, proof, source, filters, and message
92
- direction from company evidence plus user directions; newest directions win.
112
+ for the LinkedIn profile URL first, with company website as the fallback, if the
113
+ client/company is missing. After the lookup, infer buyer, offer/CTA, proof,
114
+ source, filters, and message direction from company evidence plus user
115
+ directions; newest directions win.
93
116
  Set `interactionMode: "autonomous"` once `campaignId` exists. Auto-select
94
117
  pre-launch choices when confidence is sufficient, but show the assumed choice
95
118
  briefly. Pause for missing credentials/data, failed quality floors, or final
@@ -186,9 +209,11 @@ After scouting, show a second approval gate for the concrete source action.
186
209
  For LinkedIn engagement (`signal-discovery` internally), state selected-post
187
210
  count, target engager/source-candidate volume, internal campaign-table
188
211
  execution-slice size, cleanup risk, and fallback; label the approval like
189
- "Approve scraping 3 selected LinkedIn posts?" For Sales Nav or Prospeo, name the
190
- specific search/import lane and source lead count. Do not call `import_leads` or
191
- `confirm_lead_list` until this gate is approved.
212
+ "Approve scraping N recommended LinkedIn posts?" where N is the smallest
213
+ right-content post set that clears the approved source-candidate target. Do not
214
+ default to 3 just because `selectionTarget` was 3 for sampling. For Sales Nav or
215
+ Prospeo, name the specific search/import lane and source lead count. Do not call
216
+ `import_leads` or `confirm_lead_list` until this gate is approved.
192
217
 
193
218
  For Sales Nav and Prospeo, do not ask to import only the internal 15-row
194
219
  campaign-table execution slice at the source-action gate. First-page samples are