@sellable/mcp 0.1.144 → 0.1.145

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.
package/README.md CHANGED
@@ -16,10 +16,11 @@ Each message gets 5+ minutes of Claude attention with deep research - no other t
16
16
 
17
17
  ### Prompt Source Of Truth
18
18
 
19
- There are two public Sellable entrypoints shared across hosts:
19
+ There are three public Sellable entrypoints shared across hosts:
20
20
 
21
21
  - `sellable:create-campaign`
22
22
  - `sellable:interview`
23
+ - `sellable:load-voice`
23
24
 
24
25
  The create-campaign public wrapper at
25
26
  `mcp/sellable/skills/create-campaign/SKILL.md` handles auth/bootstrap and loads
@@ -32,6 +33,11 @@ from:
32
33
 
33
34
  - `mcp/sellable/skills/interview/SKILL.md`
34
35
 
36
+ The load-voice public wrapper loads the current voice/company memory for use in
37
+ writing, application answers, posts, replies, and reviews from:
38
+
39
+ - `mcp/sellable/skills/load-voice/SKILL.md`
40
+
35
41
  Keep `create-campaign-v2` internal. Do not advertise it as a public command.
36
42
 
37
43
  ### 1. One-Command Agent Install
@@ -127,8 +133,9 @@ The installer does the full local setup:
127
133
  `mcp__sellable__*` tools into skill sessions
128
134
 
129
135
  After the installer passes, fully quit and reopen Codex Desktop. Start a new
130
- thread and select `Sellable Create Campaign` / `Sellable Identity Interview`, or
131
- invoke `$sellable:create-campaign` / `$sellable:interview`. If the app still says
136
+ thread and select `Sellable Create Campaign`, `Sellable Identity Interview`, or
137
+ `Sellable Load Voice`; or invoke `$sellable:create-campaign`,
138
+ `$sellable:interview`, or `$sellable:load-voice`. If the app still says
132
139
  `mcp__sellable__*` tools are missing after the installer passes, check that
133
140
  `~/.codex/config.toml` contains both `[marketplaces.sellable]` and
134
141
  `[plugins."sellable@sellable"]`.
@@ -141,13 +148,17 @@ Use these names consistently:
141
148
 
142
149
  - Claude Code command: `/sellable:create-campaign`
143
150
  - Claude Code command: `/sellable:interview`
151
+ - Claude Code command: `/sellable:load-voice`
144
152
  - Codex command: `$sellable:create-campaign`
145
153
  - Codex command: `$sellable:interview`
154
+ - Codex command: `$sellable:load-voice`
146
155
  - Codex Desktop plugin: `sellable@sellable`
147
156
  - Codex visible skill: `Sellable Create Campaign`
148
157
  - Codex visible skill: `Sellable Identity Interview`
158
+ - Codex visible skill: `Sellable Load Voice`
149
159
  - Codex skill frontmatter name: `create-campaign`
150
160
  - Codex skill frontmatter name: `interview`
161
+ - Codex skill frontmatter name: `load-voice`
151
162
  - MCP server name: `sellable`
152
163
  - Internal workflow prompt: `create-campaign-v2`
153
164
 
@@ -51,7 +51,8 @@ currentStep: "signal-discovery" })` before sampling so the watched Signal
51
51
  visible headline/display-name cues only. Do not enrich people during
52
52
  viability estimation.
53
53
  7. Compute capacity before recommending the source: source target good-fit
54
- leads (default 300 unless the parent supplies a target), reachable engagers,
54
+ leads (default 150 for Signal Discovery unless the parent supplies a target),
55
+ reachable engagers,
55
56
  sampled ICP-fit rate as `n/N` plus an easy percentage/range, expected usable
56
57
  leads per 100 engagers before and after a conservative dedupe/cleanup
57
58
  factor, required engagers to scrape (`source target / fit rate`), average
@@ -93,8 +94,8 @@ Evidence standards:
93
94
  exist, how many sampled engagers looked in-ICP, how many good-fit prospects
94
95
  that implies per 100 engagers, how many usable prospects one right-content
95
96
  post should yield after cleanup, how many engagers must be scraped for the
96
- 300-good-fit source target, how many posts are needed for that source target,
97
+ 150-good-fit source target, how many posts are needed for that source target,
97
98
  and which posts you would use. Also say the first campaign import remains the
98
- bounded 15-row review batch.
99
+ bounded review batch.
99
100
  - If `fetch_post_engagers` is unavailable or fails, report that explicitly and mark the estimate lower-confidence.
100
101
  - Keep LinkedIn Engagement viable when selected posts can produce roughly 150+ ICP-fit warm prospects before final filtering, even if Sales Nav is more scalable.
@@ -27,7 +27,7 @@ Process:
27
27
  1. Read the campaign brief, source intake, kickoff doc, or lane prompt supplied by the parent.
28
28
  2. Identify whether this is domain/account targeting or broad persona expansion.
29
29
  3. For domain targeting, use or create the standalone `domainFilterId` before searching; never pass raw domains directly into `search_prospeo`.
30
- 4. Run the narrowest useful Prospeo people preview and 1-2 refinements if quality or scale is unclear.
30
+ 4. Run the narrowest useful Prospeo people preview and 1-2 refinements if quality or scale is unclear. Check scale against a default 300+ good-fit target, capped at 2,500 source candidates unless the parent supplies a different target.
31
31
  5. Call out that Prospeo gives contact/account coverage but usually weaker LinkedIn intent than LinkedIn Engagement or Sales Nav activity slices.
32
32
 
33
33
  Return a concise structured result with:
@@ -28,8 +28,9 @@ Process:
28
28
  2. Preserve target role names with `CURRENT_TITLE` lookups; do not rely on seniority alone when the brief names concrete roles.
29
29
  3. When `lookup_sales_nav_filter` returns multiple title options, choose the closest semantic title match instead of the first result.
30
30
  4. Build a broad-but-reasonable baseline from role/title, geography, company size, industry/account context, and recent LinkedIn activity when relevant.
31
- 5. Check scale against the source target good-fit lead count (default 300
32
- unless the parent supplies a target). If raw preview volume or projected usable volume
31
+ 5. Check scale against the source target good-fit lead count (default 300+
32
+ for Sales Nav, capped at 2,500 source candidates unless the parent supplies a target).
33
+ If raw preview volume or projected usable volume
33
34
  is below target, do not present the tiny result as the scale fallback yet.
34
35
  Loosen nonessential filters in order: remove recent-activity first, widen
35
36
  adjacent title variants, widen geography/company-size constraints, and only
package/dist/server.js CHANGED
@@ -2,35 +2,36 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
4
  import { getSkillByName, listSkills } from "./skills.js";
5
- import { authToolDefinitions, getAuthStatus } from "./tools/auth.js";
6
- import { blueprintCommitToolDefinitions, handleAddColumn, handleCommitBlueprint, } from "./tools/blueprint-commit.js";
7
- import { bootstrapCreateCampaign, bootstrapToolDefinitions, } from "./tools/bootstrap.js";
8
- import { campaignToolDefinitions, createCampaign, getCampaign, getCampaignMessagesPreview, getCampaigns, pauseCampaign, startCampaign, updateCampaign, updateCampaignBrief, } from "./tools/campaigns.js";
9
- import { cellToolDefinitions, queueCells, updateCell } from "./tools/cells.js";
10
- import { handleStartCliLogin, handleWaitForCliLogin, startCliLoginToolDef, waitForCliLoginToolDef, } from "./tools/cli-login.js";
11
- import { contextToolDefinitions, getCampaignContext, hydrateCampaignContextFromCampaign, markCampaignContextDirty, } from "./tools/context.js";
12
- import { addToCommentCampaign, addToConnectionCampaign, addToInmailCampaign, directCampaignToolDefinitions, getEngagedPosts, getOrCreateDirectCampaignTable, pauseDirectCampaign, startDirectCampaign, } from "./tools/direct-campaigns.js";
13
- import { bootstrapEngage, bootstrapEngageMulti, engageBootstrapToolDefinitions, } from "./tools/engage-bootstrap.js";
14
- import { engageDiscoveryToolDefinitions, searchEngagementPosts, } from "./tools/engage-discovery.js";
15
- import { copySenderConfigTool, engageMemoryToolDefinitions, getEngageMemoryTool, migrateFlatConfigsTool, recordEngageProvenSearchTool, setEngageStyleGuideTool, upsertEngageTrackedPersonTool, } from "./tools/engage-memory.js";
16
- import { engageStateToolDefinitions, getEngageStateTool, setEngageStateTool, } from "./tools/engage-state.js";
17
- import { bulkEnrichWithProspeo, enrichmentToolDefinitions, enrichWithProspeo, getProspeoCredits, } from "./tools/enrichment.js";
18
- import { frameworkToolDefinitions, getCampaignFramework, } from "./tools/framework.js";
19
- import { cancelLeadImport, confirmLeadList, getProviderPrompt, importLeads, leadToolDefinitions, loadCsvDomains, loadCsvLinkedinLeads, lookupSalesNavFilter, saveDomainFilters, searchApollo, searchProspeo, searchSalesNav, searchSignals, selectPromisingPosts, setHeadlineICPCriteria, } from "./tools/leads.js";
20
- import { fetchCompany, fetchCompanyPosts, fetchLinkedInPosts, fetchLinkedInProfile, fetchPostEngagers, getLinkedInProfile, getUserPosts, linkedinToolDefinitions, } from "./tools/linkedin.js";
21
- import { getCampaignNavigationState, navigationToolDefinitions, } from "./tools/navigation.js";
22
- import { addOnDemandLeads, createOnDemandCampaign, createOnDemandTable, initOnDemandSequence, onDemandToolDefinitions, pauseOnDemandCampaign, startOnDemandCampaign, } from "./tools/one-off.js";
23
- import { processingToolDefinitions, upsertRubric } from "./tools/processing.js";
24
- import { completeSenderResearch, getMessagePrompt, getPostFindLeadsScoutRegistry, getSourceScoutRegistry, getSubskillAsset, getSubskillPrompt, listSubskillPrompts, promptToolDefinitions, searchSubskillPrompts, } from "./tools/prompts.js";
25
- import { readinessToolDefinitions, waitForCampaignTableReady, waitForLeadListReady, } from "./tools/readiness.js";
26
- import { getRows, getTableRows, getTableRowsMinimal, rowToolDefinitions, } from "./tools/rows.js";
27
- import { addRubricItem, checkRubric, deleteRubricItem, draftRubrics, rubricToolDefinitions, saveRubrics, selectNecessaryRubrics, updateRubricItem, waitForRubricResults, } from "./tools/rubrics.js";
28
- import { getSender, listSenders, senderToolDefinitions, } from "./tools/senders.js";
29
- import { attachRecommendedSequence, attachSequence, createWorkflowTable, sequencerToolDefinitions, } from "./tools/sequencer.js";
30
- import { listTables, tableToolDefinitions } from "./tools/tables.js";
31
- import { handleVerifyTableRow, verifyRowToolDefinitions, } from "./tools/verify-row.js";
5
+ import { getAuthStatus } from "./tools/auth.js";
6
+ import { handleAddColumn, handleCommitBlueprint, } from "./tools/blueprint-commit.js";
7
+ import { bootstrapCreateCampaign } from "./tools/bootstrap.js";
8
+ import { createCampaign, getCampaign, getCampaignMessagesPreview, getCampaigns, pauseCampaign, startCampaign, updateCampaign, updateCampaignBrief, } from "./tools/campaigns.js";
9
+ import { queueCells, updateCell } from "./tools/cells.js";
10
+ import { handleStartCliLogin, handleWaitForCliLogin, } from "./tools/cli-login.js";
11
+ import { getCampaignContext, hydrateCampaignContextFromCampaign, markCampaignContextDirty, } from "./tools/context.js";
12
+ import { addToCommentCampaign, addToConnectionCampaign, addToInmailCampaign, getEngagedPosts, getOrCreateDirectCampaignTable, pauseDirectCampaign, startDirectCampaign, } from "./tools/direct-campaigns.js";
13
+ import { bootstrapEngage, bootstrapEngageMulti, } from "./tools/engage-bootstrap.js";
14
+ import { searchEngagementPosts } from "./tools/engage-discovery.js";
15
+ import { copySenderConfigTool, getEngageMemoryTool, migrateFlatConfigsTool, recordEngageProvenSearchTool, setEngageStyleGuideTool, upsertEngageTrackedPersonTool, } from "./tools/engage-memory.js";
16
+ import { getEngageStateTool, setEngageStateTool, } from "./tools/engage-state.js";
17
+ import { bulkEnrichWithProspeo, enrichWithProspeo, getProspeoCredits, } from "./tools/enrichment.js";
18
+ import { getCampaignFramework } from "./tools/framework.js";
19
+ import { cancelLeadImport, confirmLeadList, getProviderPrompt, importLeads, loadCsvDomains, loadCsvLinkedinLeads, lookupSalesNavFilter, saveDomainFilters, searchApollo, searchProspeo, searchSalesNav, searchSignals, selectPromisingPosts, setHeadlineICPCriteria, } from "./tools/leads.js";
20
+ import { fetchCompany, fetchCompanyPosts, fetchLinkedInPosts, fetchLinkedInProfile, fetchPostEngagers, getLinkedInProfile, getUserPosts, } from "./tools/linkedin.js";
21
+ import { getCampaignNavigationState } from "./tools/navigation.js";
22
+ import { addOnDemandLeads, createOnDemandCampaign, createOnDemandTable, initOnDemandSequence, pauseOnDemandCampaign, startOnDemandCampaign, } from "./tools/one-off.js";
23
+ import { upsertRubric } from "./tools/processing.js";
24
+ import { completeSenderResearch, getMessagePrompt, getPostFindLeadsScoutRegistry, getSourceScoutRegistry, getSubskillAsset, getSubskillPrompt, listSubskillPrompts, searchSubskillPrompts, } from "./tools/prompts.js";
25
+ import { waitForCampaignTableReady, waitForLeadListReady, } from "./tools/readiness.js";
26
+ import { allTools } from "./tools/registry.js";
27
+ import { getRows, getTableRows, getTableRowsMinimal } from "./tools/rows.js";
28
+ import { addRubricItem, checkRubric, deleteRubricItem, draftRubrics, saveRubrics, selectNecessaryRubrics, updateRubricItem, waitForRubricResults, } from "./tools/rubrics.js";
29
+ import { getSender, listSenders } from "./tools/senders.js";
30
+ import { attachRecommendedSequence, attachSequence, createWorkflowTable, } from "./tools/sequencer.js";
31
+ import { listTables } from "./tools/tables.js";
32
+ import { handleVerifyTableRow } from "./tools/verify-row.js";
32
33
  import { sanitizeWatchUrlsForMcpResult } from "./tools/watch-url-security.js";
33
- import { addTeammate, createWorkspace, getActiveWorkspace, listWorkspaces, setActiveWorkspace, workspaceToolDefinitions, } from "./tools/workspaces.js";
34
+ import { addTeammate, createWorkspace, getActiveWorkspace, listWorkspaces, setActiveWorkspace, } from "./tools/workspaces.js";
34
35
  import { checkForUpdates, logUpdateNotice } from "./update-check.js";
35
36
  const server = new Server({
36
37
  name: "sellable-mcp",
@@ -41,37 +42,6 @@ const server = new Server({
41
42
  prompts: {},
42
43
  },
43
44
  });
44
- const allTools = [
45
- ...campaignToolDefinitions,
46
- ...authToolDefinitions,
47
- startCliLoginToolDef,
48
- waitForCliLoginToolDef,
49
- ...bootstrapToolDefinitions,
50
- ...engageBootstrapToolDefinitions,
51
- ...engageDiscoveryToolDefinitions,
52
- ...workspaceToolDefinitions,
53
- ...frameworkToolDefinitions,
54
- ...contextToolDefinitions,
55
- ...navigationToolDefinitions,
56
- ...leadToolDefinitions,
57
- ...enrichmentToolDefinitions,
58
- ...processingToolDefinitions,
59
- ...rubricToolDefinitions,
60
- ...readinessToolDefinitions,
61
- ...rowToolDefinitions,
62
- ...cellToolDefinitions,
63
- ...promptToolDefinitions,
64
- ...linkedinToolDefinitions,
65
- ...onDemandToolDefinitions,
66
- ...directCampaignToolDefinitions,
67
- ...senderToolDefinitions,
68
- ...engageStateToolDefinitions,
69
- ...engageMemoryToolDefinitions,
70
- ...sequencerToolDefinitions,
71
- ...tableToolDefinitions,
72
- ...blueprintCommitToolDefinitions,
73
- ...verifyRowToolDefinitions,
74
- ];
75
45
  function parseOptionalNumber(value) {
76
46
  if (typeof value === "number" && Number.isFinite(value))
77
47
  return value;
@@ -22,7 +22,7 @@ function appendUpdateNotice(notice, update) {
22
22
  export const authToolDefinitions = [
23
23
  {
24
24
  name: "get_auth_status",
25
- description: "Verify Sellable authentication early. Call this before any campaign work. Also performs a cached Sellable package update check. Returns ok=false with clear guidance when the token is missing, revoked, or the active workspace is not set. IMPORTANT: When auth succeeds, ALWAYS tell the user the _userNotice message so they know which workspace is active and whether an update is available. CRITICAL: When auth FAILS with a missing/invalid token (error.type is 'config' or 'auth'), the response includes an `agentInstruction` field with the verbatim FTUX magic-link signup flow. Follow agentInstruction VERBATIM. Do NOT show the user error.guidance — that's a fallback for non-interactive contexts only.",
25
+ description: "Verify Sellable authentication early. Call this before any campaign work. Also performs a cached Sellable package update check. Returns ok=false with clear guidance when the token is missing, revoked, or the active workspace is not set. When auth succeeds, use _userNotice when workspace context is useful; it is written in customer-facing product language and may include update guidance. CRITICAL: When auth FAILS with a missing/invalid token (error.type is 'config' or 'auth'), the response includes an `agentInstruction` field with the verbatim FTUX magic-link signup flow. Follow agentInstruction VERBATIM. Do NOT show the user error.guidance — that's a fallback for non-interactive contexts only.",
26
26
  inputSchema: {
27
27
  type: "object",
28
28
  properties: {},
@@ -92,8 +92,8 @@ export async function getAuthStatus() {
92
92
  const envLabel = config.activeEnvName || null;
93
93
  const wsLabel = workspaceName || activeWorkspaceId;
94
94
  const notice = envLabel
95
- ? `Workspace: ${wsLabel} (${envLabel})`
96
- : `Workspace: ${wsLabel}`;
95
+ ? `I’m building this in ${wsLabel} (${envLabel}).`
96
+ : `I’m building this in ${wsLabel}.`;
97
97
  return {
98
98
  ok: true,
99
99
  configPath,
@@ -138,8 +138,8 @@ export async function getAuthStatus() {
138
138
  "5) Call `mcp__sellable__wait_for_cli_login({ sessionId })` using the sessionId returned by start_cli_login. " +
139
139
  "6) If the result is `error.type === 'tool_timeout_guard'`, IMMEDIATELY re-call wait_for_cli_login with the SAME sessionId — do not narrate, do not call start_cli_login again. Loop until you get a different result. " +
140
140
  "7) On `ok: true`, the user is signed in and `~/.sellable/config.json` has been written. Branch on `isReturningUser`: " +
141
- "if true, say `You're in — {activeWorkspaceName} workspace, ready to roll.\\n\\nNow — paste the LinkedIn profile URL of the person you'll be sending campaigns from. Usually that's you (the founder), or whoever's voice the messages should sound like.\\n\\ne.g. https://www.linkedin.com/in/your-handle`; " +
142
- "if false, say `You're set up — your {activeWorkspaceName} workspace is ready.\\n\\nNow — paste the LinkedIn profile URL of the person you'll be sending campaigns from. Usually that's you (the founder), or whoever's voice the messages should sound like.\\n\\ne.g. https://www.linkedin.com/in/your-handle`";
141
+ "if true, say `You're in — {activeWorkspaceName} workspace, ready to roll.\\n\\nNow — 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.\\n\\ne.g. https://example.com or https://www.linkedin.com/in/client-handle`; " +
142
+ "if false, say `You're set up — your {activeWorkspaceName} workspace is ready.\\n\\nNow — 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.\\n\\ne.g. https://example.com or https://www.linkedin.com/in/client-handle`";
143
143
  if (error instanceof SellableApiError && error.isAuthError) {
144
144
  return {
145
145
  ...base,
@@ -14,10 +14,11 @@ const workspaceRoot = resolveWorkspaceRoot(entryDir);
14
14
  const skillsRoot = resolveSkillsDir();
15
15
  const signalProviderConfigPath = join(skillsRoot, "create-campaign/core/providers/signal-discovery.json");
16
16
  const leadImportLimitsPath = join(workspaceRoot, "lead-import-limits.json");
17
+ const campaignSourceDefaultsPath = join(workspaceRoot, "campaign-source-defaults.json");
17
18
  const defaultLeadImportLimits = {
18
19
  apollo: { maxImportCount: 2500 },
19
20
  "sales-nav": { maxImportCount: 2500 },
20
- prospeo: { maxImportCount: 10000 },
21
+ prospeo: { maxImportCount: 2500 },
21
22
  "signal-discovery": { maxImportCount: 2500 },
22
23
  };
23
24
  const defaultSignalDiscoveryConfig = {
@@ -25,6 +26,28 @@ const defaultSignalDiscoveryConfig = {
25
26
  promisingPostsTarget: 3,
26
27
  },
27
28
  };
29
+ const defaultCampaignSourceDefaults = {
30
+ reviewBatch: {
31
+ defaultSize: 25,
32
+ minProjectedPass: 5,
33
+ },
34
+ providers: {
35
+ "signal-discovery": {
36
+ targetGoodFitLeads: 150,
37
+ defaultFitRate: 0.15,
38
+ maxSourceCandidates: 2500,
39
+ postCoverageBuffer: 1.2,
40
+ },
41
+ "sales-nav": {
42
+ targetGoodFitLeads: 300,
43
+ maxSourceCandidates: 2500,
44
+ },
45
+ prospeo: {
46
+ targetGoodFitLeads: 300,
47
+ maxSourceCandidates: 2500,
48
+ },
49
+ },
50
+ };
28
51
  const prospeoFilterValueSchema = {
29
52
  type: "object",
30
53
  description: "Include/exclude list filter (values must match Prospeo enums)",
@@ -245,6 +268,39 @@ function loadLeadImportLimits() {
245
268
  return defaultLeadImportLimits;
246
269
  }
247
270
  }
271
+ function loadCampaignSourceDefaults() {
272
+ if (!existsSync(campaignSourceDefaultsPath)) {
273
+ return defaultCampaignSourceDefaults;
274
+ }
275
+ try {
276
+ const raw = readFileSync(campaignSourceDefaultsPath, "utf-8");
277
+ const parsed = JSON.parse(raw);
278
+ return {
279
+ reviewBatch: {
280
+ ...defaultCampaignSourceDefaults.reviewBatch,
281
+ ...(parsed.reviewBatch ?? {}),
282
+ },
283
+ providers: {
284
+ ...defaultCampaignSourceDefaults.providers,
285
+ ...(parsed.providers ?? {}),
286
+ },
287
+ };
288
+ }
289
+ catch {
290
+ return defaultCampaignSourceDefaults;
291
+ }
292
+ }
293
+ function getSignalDiscoverySourcePlanDefaults() {
294
+ const defaults = loadCampaignSourceDefaults();
295
+ const signalDefaults = defaults.providers["signal-discovery"];
296
+ const sourceCandidateTarget = Math.min(Math.ceil(signalDefaults.targetGoodFitLeads / signalDefaults.defaultFitRate), signalDefaults.maxSourceCandidates);
297
+ return {
298
+ targetGoodFitLeads: signalDefaults.targetGoodFitLeads,
299
+ defaultFitRate: signalDefaults.defaultFitRate,
300
+ sourceCandidateTarget,
301
+ reviewBatchSize: defaults.reviewBatch.defaultSize,
302
+ };
303
+ }
248
304
  function getMaxImportCount(provider) {
249
305
  const limits = loadLeadImportLimits();
250
306
  const max = limits[provider]?.maxImportCount;
@@ -443,16 +499,16 @@ function buildSourceImportWatchNarration({ provider, selectedPostCount, estimate
443
499
  : ""}`
444
500
  : `the approved ${providerLabel} source`;
445
501
  const targetDetail = typeof targetLeadCount === "number"
446
- ? ` Targeting ${targetLeadCount.toLocaleString("en-US")} source leads before the bounded review batch is cloned.`
502
+ ? ` Targeting ${targetLeadCount.toLocaleString("en-US")} source candidates before the bounded review batch is cloned.`
447
503
  : "";
448
504
  return {
449
505
  stage: "review-batch",
450
506
  headline: provider === "signal-discovery"
451
- ? "Scraping source leads from posts"
452
- : "Importing source leads",
507
+ ? "Scraping source candidates from posts"
508
+ : "Importing source candidates",
453
509
  visibleState: `The browser is showing ${providerLabel} import progress for ${sourceDetail}.${targetDetail}`,
454
510
  agentIntent: "Codex is materializing the approved source into a lead list before cloning only the bounded review batch into the campaign.",
455
- nextAction: "Wait for source leads, then import the 15-row review batch",
511
+ nextAction: "Wait for source candidates, then import the review batch",
456
512
  safety: "Import is limited to the review batch.",
457
513
  };
458
514
  }
@@ -537,8 +593,6 @@ function buildSelectedPostApprovalWatchNarration(selectedPostCount) {
537
593
  safety: "Scrape approval is the next gate.",
538
594
  };
539
595
  }
540
- const SOURCE_RECOMMENDATION_TARGET = 300;
541
- const SOURCE_RECOMMENDATION_FIT_RATE = 0.15;
542
596
  function escapeMarkdownTableCell(value) {
543
597
  return (value || "-").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
544
598
  }
@@ -547,6 +601,7 @@ function formatApproxInteger(value) {
547
601
  return `~${rounded.toLocaleString("en-US")}`;
548
602
  }
549
603
  function buildSignalDiscoverySourceRecommendation({ selectedPosts, }) {
604
+ const { targetGoodFitLeads, defaultFitRate, sourceCandidateTarget, reviewBatchSize, } = getSignalDiscoverySourcePlanDefaults();
550
605
  const selectedCount = selectedPosts.length;
551
606
  const totalEngagement = selectedPosts.reduce((sum, post) => sum + (post.likes ?? 0) + (post.comments ?? 0), 0);
552
607
  const tableRows = selectedPosts
@@ -555,15 +610,14 @@ function buildSignalDiscoverySourceRecommendation({ selectedPosts, }) {
555
610
  return `| ${escapeMarkdownTableCell(post.authorName)} | ${escapeMarkdownTableCell(post.reason)} | ${formatApproxInteger(engagement)} |`;
556
611
  })
557
612
  .join("\n");
558
- const engagersNeeded = Math.ceil(SOURCE_RECOMMENDATION_TARGET / SOURCE_RECOMMENDATION_FIT_RATE);
559
- const estimatedGoodFit = totalEngagement * SOURCE_RECOMMENDATION_FIT_RATE;
613
+ const estimatedGoodFit = totalEngagement * defaultFitRate;
560
614
  return `## Source Recommendation
561
615
 
562
616
  Use Signal Discovery first.
563
617
 
564
- **Goal:** ~${SOURCE_RECOMMENDATION_TARGET.toLocaleString("en-US")} good-fit prospects after cleanup, enrichment, and filters<br>
565
- **Working assumption:** ~${Math.round(SOURCE_RECOMMENDATION_FIT_RATE * 100)}% of raw post engagers become good-fit prospects<br>
566
- **Engagers needed:** ~${engagersNeeded.toLocaleString("en-US")} raw engagers
618
+ **Good-fit target:** ~${targetGoodFitLeads.toLocaleString("en-US")} prospects after cleanup, enrichment, and filters<br>
619
+ **Source-candidate plan:** scrape ~${sourceCandidateTarget.toLocaleString("en-US")} raw engagers using a conservative ${Math.round(defaultFitRate * 100)}% fit-rate assumption<br>
620
+ **Review checkpoint:** import the first ${reviewBatchSize.toLocaleString("en-US")} leads into the campaign for fit and message review before scaling
567
621
 
568
622
  ### Selected posts
569
623
 
@@ -572,15 +626,15 @@ Use Signal Discovery first.
572
626
  ${tableRows || "| Selected posts | Campaign-matched public engagement | - |"}
573
627
 
574
628
  **Total visible pool:** ${formatApproxInteger(totalEngagement)} engagers<br>
575
- **Estimated good-fit pool at ${Math.round(SOURCE_RECOMMENDATION_FIT_RATE * 100)}%:** ${formatApproxInteger(estimatedGoodFit)} prospects before dedupe/risk cleanup
629
+ **Estimated good-fit pool at ${Math.round(defaultFitRate * 100)}%:** ${formatApproxInteger(estimatedGoodFit)} prospects before dedupe/risk cleanup
576
630
 
577
631
  ### Recommendation
578
632
 
579
633
  Approve scraping these ${selectedCount} posts.
580
634
 
581
- This gives enough volume to target ~${SOURCE_RECOMMENDATION_TARGET.toLocaleString("en-US")} good-fit prospects after cleanup, while keeping the source tied to people already engaging with the campaign's strongest public buying signals.
635
+ This gives enough volume to work toward ~${targetGoodFitLeads.toLocaleString("en-US")} good-fit prospects while keeping the source tied to people already engaging with the campaign's strongest public buying signals.
582
636
 
583
- **First pass:** build the source list, then import only 15 leads into the review batch so we can inspect quality before scaling.
637
+ **First pass:** build the source list, then import only the ${reviewBatchSize.toLocaleString("en-US")}-lead review batch so we can inspect fit and messages before scaling.
584
638
 
585
639
  **Fallback:** if the review batch is too vendor-heavy, agency-heavy, or off-ICP, switch to Sales Nav recent activity.
586
640
 
@@ -1168,7 +1222,7 @@ export const leadToolDefinitions = [
1168
1222
  },
1169
1223
  targetEngagerCount: {
1170
1224
  type: "number",
1171
- description: "Signal Discovery: target number of post engagers to scrape. Compute from source target good-fit leads / sampled fit rate; e.g. 300 good fits at 20% fit requires about 1500 engagers. Limits selected posts before starting scrape.",
1225
+ description: "Signal Discovery: target number of post engagers/source candidates to scrape. Compute from source target good-fit leads / sampled fit rate; e.g. 150 good fits at 15% fit requires about 1000 engagers. Limits selected posts before starting scrape.",
1172
1226
  },
1173
1227
  maxPostsToScrape: {
1174
1228
  type: "number",
@@ -2811,13 +2865,14 @@ export async function selectPromisingPosts(input) {
2811
2865
  });
2812
2866
  }
2813
2867
  catch {
2868
+ const { reviewBatchSize } = getSignalDiscoverySourcePlanDefaults();
2814
2869
  sourceRecommendation = `## Source Recommendation
2815
2870
 
2816
2871
  Use Signal Discovery first.
2817
2872
 
2818
2873
  **Recommendation:** approve scraping the ${selectionResult.selectedCount} selected Signal Discovery post${selectionResult.selectedCount === 1 ? "" : "s"}.
2819
2874
 
2820
- **First pass:** build the source list, then import only 15 leads into the review batch so we can inspect quality before scaling.
2875
+ **First pass:** build the source list, then import only the ${reviewBatchSize.toLocaleString("en-US")}-lead review batch so we can inspect fit and messages before scaling.
2821
2876
 
2822
2877
  Approval card should say:
2823
2878