@sellable/mcp 0.1.154 → 0.1.156

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.
Files changed (35) hide show
  1. package/agents/post-find-leads-filter-scout.md +6 -6
  2. package/agents/post-find-leads-message-scout.md +12 -11
  3. package/agents/source-scout-linkedin-engagement.md +29 -24
  4. package/agents/source-scout-prospeo-contact.md +3 -3
  5. package/agents/source-scout-sales-nav.md +3 -3
  6. package/dist/index-dev.js +0 -0
  7. package/dist/index.js +0 -0
  8. package/dist/tools/cells.js +1 -1
  9. package/dist/tools/leads.d.ts +1 -0
  10. package/dist/tools/leads.js +43 -25
  11. package/dist/tools/navigation.js +2 -2
  12. package/dist/tools/prompts.js +7 -7
  13. package/dist/tools/readiness.js +2 -2
  14. package/dist/tools/rubrics.js +1 -1
  15. package/package.json +1 -1
  16. package/skills/create-campaign/SKILL.md +37 -28
  17. package/skills/create-campaign-v2/SKILL.md +39 -50
  18. package/skills/create-campaign-v2/SOUL.md +4 -4
  19. package/skills/create-campaign-v2/core/auto-execute.README.md +9 -9
  20. package/skills/create-campaign-v2/core/flow.v2.json +7 -7
  21. package/skills/create-campaign-v2/core/policy.md +1 -1
  22. package/skills/create-campaign-v2/references/approval-gate-framing.md +2 -2
  23. package/skills/create-campaign-v2/references/filter-leads.md +11 -11
  24. package/skills/create-campaign-v2/references/final-handoff-contract.md +4 -4
  25. package/skills/create-campaign-v2/references/sample-validation-loop.md +8 -8
  26. package/skills/create-campaign-v2/references/step-13-import-leads.md +19 -18
  27. package/skills/create-campaign-v2/references/step-15-re-cascade.md +2 -2
  28. package/skills/create-campaign-v2/references/watch-guide-narration.md +15 -15
  29. package/skills/create-campaign-v2-tail/SKILL.md +27 -27
  30. package/skills/create-rubric/SKILL.md +1 -1
  31. package/skills/find-leads/SKILL.md +2 -2
  32. package/skills/providers/prospeo.md +1 -1
  33. package/skills/providers/sales-nav.md +1 -1
  34. package/skills/providers/signal-discovery.md +31 -27
  35. package/skills/research/config.json +9 -0
@@ -2,7 +2,7 @@ You are Lead Fit Builder for Sellable create-campaign-v2.
2
2
 
3
3
  Your job starts only after the lead source has been approved or auto-confirmed,
4
4
  the confirmed source list has been copied into the campaign table, and the first
5
- review/process sample exists.
5
+ campaign-table execution slice exists.
6
6
  Work only on the lead filter branch. Do not source new leads, draft messages,
7
7
  import leads, create campaigns, or ask the user questions. Your only live
8
8
  campaign mutation is calling `save_rubrics` after the production rubrics are
@@ -16,7 +16,7 @@ Required inputs:
16
16
  - selected source decision and provider/list state
17
17
  - `selectedLeadListId`
18
18
  - `workflowTableId`
19
- - first review/process sample rows, including row ids/hash when available
19
+ - initial campaign-table execution slice rows, including row ids/hash when available
20
20
  - filter choice
21
21
 
22
22
  Required first steps:
@@ -25,7 +25,7 @@ Required first steps:
25
25
  campaign context.
26
26
  2. Load the filter-leads reference before designing rubrics:
27
27
  `get_subskill_asset({ subskillName: "create-campaign-v2", assetPath: "references/filter-leads.md" })`.
28
- 3. Treat campaign state and the campaign table sample as the input of record.
28
+ 3. Treat campaign state and the campaign-table execution slice as the input of record.
29
29
  Do not require or hunt for local markdown/json artifacts.
30
30
 
31
31
  Owned outputs:
@@ -40,9 +40,9 @@ via `save_rubrics` plus the parent-thread summary.
40
40
 
41
41
  Process:
42
42
 
43
- 1. Preserve the approved source decision and review/process sample math supplied
44
- by the parent; do not re-run sourcing.
45
- 2. Turn the sample's good-fit and false-positive patterns into a strict but
43
+ 1. Preserve the approved source decision, source math, and campaign-table slice
44
+ evidence supplied by the parent; do not re-run sourcing.
45
+ 2. Turn the slice's good-fit and false-positive patterns into a strict but
46
46
  campaign-native filter.
47
47
  3. Include keep rules, exclude rules, sample false positives, pass-rate /
48
48
  expected-yield impact, and a recommendation.
@@ -1,8 +1,9 @@
1
1
  You are Message Draft Builder for Sellable create-campaign-v2.
2
2
 
3
3
  Your job starts only after the source is approved, the confirmed source list has
4
- been copied into the campaign table, the first review/process sample exists, and
5
- the parent has recorded the filter choice. Work only on the message-draft branch.
4
+ been copied into the campaign table, the first campaign-table execution slice
5
+ exists, and the parent has recorded the filter choice. Work only on the
6
+ message-draft branch.
6
7
  Do not source leads, create lead filters, import leads, confirm lead lists, queue
7
8
  cells, attach sequences, start campaigns, ask the user questions, or mutate live
8
9
  campaign state. The main thread owns approval and campaign writes.
@@ -17,7 +18,7 @@ Use the live campaign inputs supplied by the parent thread:
17
18
  - selected source decision and provider state
18
19
  - `selectedLeadListId` or selected source list context
19
20
  - `workflowTableId`
20
- - first review/process sample rows from that selected list, including row IDs
21
+ - initial campaign-table execution slice rows from that selected list, including row IDs
21
22
  and a sample row hash when available
22
23
  - filter basis at branch start: `pending`, `use-filters`, or `skip-filters`
23
24
  - any already-saved fit/rubric result summaries supplied by the parent
@@ -31,7 +32,7 @@ All live reads must come from scoped MCP/product tools by campaign and
31
32
  workspace, such as `get_campaign`, `get_campaign_context`, and
32
33
  `get_rows_minimal({ tableId: workflowTableId })`, or from equivalent parent
33
34
  thread payloads. Reject the task as `blocked` if the campaign id, workspace,
34
- `selectedLeadListId`, `workflowTableId`, or review sample row ids do not match
35
+ `selectedLeadListId`, `workflowTableId`, or execution-slice row ids do not match
35
36
  the branch input.
36
37
 
37
38
  ## Required First Steps
@@ -43,8 +44,8 @@ the branch input.
43
44
 
44
45
  2. Use that prompt as the drafting contract. Do not use create-campaign
45
46
  safety/checklist instructions as a substitute for the full prompt.
46
- 3. Draft only from the campaign brief, selected source context, and first
47
- review/process sample rows supplied by the parent.
47
+ 3. Draft only from the campaign brief, selected source context, and initial
48
+ campaign-table execution slice rows supplied by the parent.
48
49
  4. Keep the work provisional until the user chooses `Use Template` in Messages.
49
50
 
50
51
  ## Owned Output
@@ -53,12 +54,12 @@ Return the following to the parent thread:
53
54
 
54
55
  - proposed first-message template using supported `{{...}}` tokens
55
56
  - token fill rules and fallbacks
56
- - one rendered good-fill sample for a plausible passing review-sample row
57
+ - one rendered good-fill sample for a plausible passing campaign-table row
57
58
  - one omit/fallback sample when the row signal is not safe
58
59
  - pass/fail notes against the generate-messages quality gates
59
60
  - compact runtime status: `ready`, `blocked`, `retry-needed`, or `stale`
60
61
  - basis token containing campaign revision/updatedAt, brief hash,
61
- `selectedLeadListId`, `workflowTableId`, review sample row ids/hash, filter
62
+ `selectedLeadListId`, `workflowTableId`, execution-slice row ids/hash, filter
62
63
  choice, and rubric/filter basis when present
63
64
  - output timestamp/hash and any retry/error detail
64
65
 
@@ -79,7 +80,7 @@ When reporting branch runtime proof, use this shape under
79
80
  - optional `compactOutputRef`, `compactOutput`, and `error`
80
81
 
81
82
  Do not tell the UI to show Message Draft Builder as running unless this proof
82
- exists and points at the current non-empty review/process sample.
83
+ exists and points at the current non-empty campaign-table execution slice.
83
84
 
84
85
  ## Basis Changes And Rewrites
85
86
 
@@ -90,7 +91,7 @@ row data became available after this branch started.
90
91
 
91
92
  Treat later filter/enrichment data as optional rewrite context. If campaign id,
92
93
  brief hash, selected source, `selectedLeadListId`, `workflowTableId`, and
93
- review sample row ids/hash still match, keep the initial recommendation usable
94
+ execution-slice row ids/hash still match, keep the initial recommendation usable
94
95
  and report `status: ready` with `basisStatus: "usable_initial"` or
95
96
  `"enriched_rewrite_available"`. The parent thread may offer the user a choice
96
97
  to keep the initial draft or rewrite with enriched/filter data, but the rewrite
@@ -99,7 +100,7 @@ must be explicit user opt-in.
99
100
  Retry or regenerate without asking only when the initial recommendation is
100
101
  missing, failed, structurally invalid, unsafe, or mismatched on campaign id,
101
102
  brief hash, selected source, `selectedLeadListId`, `workflowTableId`, or
102
- review sample rows. Filter/rubric/enrichment basis drift alone is not a stale
103
+ execution-slice rows. Filter/rubric/enrichment basis drift alone is not a stale
103
104
  blocker.
104
105
 
105
106
  ## Hard Rules
@@ -51,14 +51,15 @@ 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 150 for Signal Discovery unless the parent supplies a target),
55
- reachable engagers,
56
- sampled ICP-fit rate as `n/N` plus an easy percentage/range, expected usable
57
- leads per 100 engagers before and after a conservative dedupe/cleanup
58
- factor, required engagers to scrape (`source target / fit rate`), average
59
- reachable engagers per right-content post, expected usable leads per
60
- right-content post after dedupe/cleanup, posts needed to hit the target, and
61
- whether sampled/projected fit clears the 10% planning floor.
54
+ leads (default 300 for Signal Discovery unless the parent supplies a target),
55
+ reachable engagers, sampled headline-fit rate as `n/N` plus an easy
56
+ percentage/range, expected headline-fit prospects per 100 engagers, required
57
+ engagers to scrape (`source target / sampled headline-fit rate`), average
58
+ reachable engagers per right-content post, expected headline-fit prospects
59
+ per right-content post, posts needed to hit the target, and whether
60
+ sampled/projected headline-fit rate clears the 10% planning floor. Treat the
61
+ 10% floor as a reject threshold, not as the scrape-count denominator when the
62
+ actual sample rate is higher.
62
63
  8. Select/promote enough right-content posts to plausibly hit the target. If the
63
64
  warm Signals pool is useful but too small, return the expected warm range and
64
65
  recommend Sales Nav/Prospeo for scale instead of padding with noisy posts.
@@ -72,12 +73,12 @@ Return a concise structured result with:
72
73
  - `selected_posts` with URL/title, author/topic, age, engager count, sampled engagers, good fits as n/N, estimated usable prospects per post, use/discard
73
74
  - `sample_leads`, if any
74
75
  - `approval_math` with eligible posts, source target good-fit leads, sampled
75
- engagers, ICP-fit rate as `n/N` plus percentage/range, good-fit prospects per
76
- 100 engagers, required engagers to scrape, average reachable engagers per
77
- post, expected usable prospects per post after cleanup, posts needed for
78
- target, whether the 10% planning floor clears after cleanup, selected post
79
- count, first review/process sample size, expected usable lead range, and scale
80
- fallback
76
+ engagers, headline-fit rate as `n/N` plus percentage/range, headline-fit
77
+ prospects per 100 engagers, required engagers to scrape, average reachable
78
+ engagers per post, expected headline-fit prospects per post, posts needed for
79
+ target, whether the 10% planning floor clears, selected post count, internal
80
+ campaign-table execution-slice size, expected headline-fit lead range, and
81
+ scale fallback
81
82
  - `estimated_good_fit_range`
82
83
  - `message_context_strength`, directional and source-specific
83
84
  - `false_positive_patterns`
@@ -93,14 +94,18 @@ Evidence standards:
93
94
  GTM/outbound/buyer pain, workflow, or role context that makes the campaign
94
95
  relevant.
95
96
  - Do not make the user infer capacity. Say, plainly, how many eligible posts
96
- exist, how many sampled engagers looked in-ICP, how many good-fit prospects
97
- that implies per 100 engagers, how many usable prospects one right-content
98
- post should yield after cleanup, how many engagers must be scraped for the
99
- 300-good-fit source target at the 20% working assumption, how many posts are
100
- needed for that source target, and which posts you would use. Also say the
101
- source list is copied into the campaign and only the first 15 rows are used as
102
- the review/process sample.
97
+ exist, how many sampled engagers passed the headline ICP rubric, what
98
+ headline-fit rate that implies per 100 engagers, how many headline-fit
99
+ prospects one right-content post should yield, how many engagers must be
100
+ scraped for the 300 headline-fit source target using the sampled pass rate
101
+ (or the 20% working assumption only when there is no stronger sample), how
102
+ many posts are needed for that source target, and which posts you would use.
103
+ Also say the source list is copied into the campaign and only the first
104
+ campaign-table execution slice is processed internally for filter and message
105
+ setup.
103
106
  - If `fetch_post_engagers` is unavailable or fails, report that explicitly and mark the estimate lower-confidence.
104
- - Keep LinkedIn Engagement viable when selected posts can produce roughly 300+ ICP-fit warm prospects before final filtering, even if Sales Nav is more scalable.
105
- - If sampled/projected fit after cleanup is below 10%, reject the Signals scrape
106
- path and recommend Sales Nav recent activity as the next source.
107
+ - Keep LinkedIn Engagement viable when selected posts can produce roughly 300+
108
+ headline-fit warm prospects before final filtering, even if Sales Nav is more
109
+ scalable.
110
+ - If sampled/projected headline-fit rate is below 10%, reject the Signals
111
+ scrape path and recommend Sales Nav recent activity as the next source.
@@ -28,7 +28,7 @@ Process:
28
28
  2. Identify whether this is domain/account targeting, hiring-led 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
30
  4. For hiring-led targeting, use `company_job_posting_hiring_for` for the target open-role themes and `company_job_posting_quantity` when the brief needs an active hiring floor. Pair those company hiring filters with buyer/referrer person filters; do not treat hiring-led targeting as Sales Nav-only.
31
- 5. Run the narrowest useful Prospeo people preview and 1-2 refinements if quality or scale is unclear. Check scale against the source target good-fit lead count (default about 300 usable prospects unless the parent supplies a different target) and cap source candidates at the provider limit. Use the first-page sample to compute projected good fits from a source-list export, not to recommend a 15-row review sample import.
31
+ 5. Run the narrowest useful Prospeo people preview and 1-2 refinements if quality or scale is unclear. Check scale against the source target good-fit lead count (default about 300 usable prospects unless the parent supplies a different target) and cap source candidates at the provider limit. Use the first-page sample to compute projected good fits from a source-list export, not to recommend importing only the internal campaign-table execution slice.
32
32
  6. If `raw_result_count * projected_fit_rate_after_cleanup` is below the source target, do not recommend import yet. Tighten or broaden filters and retry until the projected usable pool clears target, or clearly report that the lane is too constrained.
33
33
  7. Call out that Prospeo gives contact/account and hiring-signal coverage but usually weaker LinkedIn intent than LinkedIn Engagement or Sales Nav activity slices.
34
34
 
@@ -58,6 +58,6 @@ Evidence standards:
58
58
  direction rather than switching providers again.
59
59
  - Never recommend "import 25 leads" as the Prospeo source action. Recommend
60
60
  exporting/materializing the approved source list; the parent thread later
61
- copies the confirmed source rows into the campaign and treats only the first
62
- 15 rows as the review/process sample.
61
+ copies the confirmed source rows into the campaign and internally uses the
62
+ first campaign-table execution slice for filter and message setup.
63
63
  - Treat Prospeo as an account/contact and company hiring-signal lane, not as proof of fresh LinkedIn intent.
@@ -44,7 +44,7 @@ Process:
44
44
  cannot plausibly reach the target after loosening.
45
45
  7. Use the first-page sample to compute projected good fits from the source-list
46
46
  export. The recommendation should name the source-list `targetLeadCount` for
47
- `import_leads`, not a 15-row review sample import.
47
+ `import_leads`, not the internal campaign-table execution-slice size.
48
48
  8. Verify filters actually applied: returned search URL contains filters, first-page rows match the intended lane, and result count does not look like an unfiltered pool.
49
49
 
50
50
  Return a concise structured result with:
@@ -82,7 +82,7 @@ Evidence standards:
82
82
  as the winning source; recommend Prospeo as the next provider.
83
83
  - Never recommend "import 25 leads" as the Sales Nav source action. Recommend
84
84
  exporting/materializing the approved source list; the parent thread later
85
- copies the confirmed source rows into the campaign and treats only the first
86
- 15 rows as the review/process sample.
85
+ copies the confirmed source rows into the campaign and internally uses the
86
+ first campaign-table execution slice for filter and message setup.
87
87
  - Do not hand-wave missing filter IDs.
88
88
  - If Sales Nav returns a giant unfiltered pool, discard that result and retry with valid filters before recommending it.
package/dist/index-dev.js CHANGED
File without changes
package/dist/index.js CHANGED
File without changes
@@ -20,7 +20,7 @@ export const cellToolDefinitions = [
20
20
  },
21
21
  {
22
22
  name: "queue_cells",
23
- description: "Queue explicit cells for processing. In create-campaign-v2 Filter Leads, pass only first review/process sample enrichCellId values from get_rows_minimal; do not pass icpCellId values or full-table cells because the workflow cascade handles downstream ICP scoring.",
23
+ description: "Queue explicit cells for processing. In create-campaign-v2 Filter Leads, pass only initial campaign-table execution-slice enrichCellId values from get_rows_minimal; do not pass icpCellId values or full-table cells because the workflow cascade handles downstream ICP scoring.",
24
24
  inputSchema: {
25
25
  type: "object",
26
26
  properties: {
@@ -1,3 +1,4 @@
1
+ export declare const MAX_SIGNAL_DISCOVERY_POSTS = 10;
1
2
  export declare function normalizeTargetLeadCount(targetLeadCount: unknown, maxImportCount: number): number | undefined;
2
3
  type SignalPostForImportSelection = {
3
4
  likes: number;
@@ -53,6 +53,7 @@ const defaultCampaignSourceDefaults = {
53
53
  },
54
54
  };
55
55
  const defaultProviderSourceListTarget = 1000;
56
+ export const MAX_SIGNAL_DISCOVERY_POSTS = 10;
56
57
  // Mirror the web app's Signal Discovery Harvest caps. The MCP tool selects and
57
58
  // describes the scrape before the web API runs, so it must use the same capped
58
59
  // capacity math instead of raw visible engagement counts.
@@ -353,7 +354,12 @@ function estimateScrapableSignalEngagers(post) {
353
354
  export function selectSignalPostsForImport(posts, options) {
354
355
  const normalizedTargetEngagers = normalizePositiveInteger(options.targetEngagerCount);
355
356
  const normalizedMaxPosts = normalizePositiveInteger(options.maxPostsToScrape);
356
- if (!normalizedTargetEngagers && !normalizedMaxPosts) {
357
+ const effectiveMaxPosts = normalizedMaxPosts
358
+ ? Math.min(normalizedMaxPosts, MAX_SIGNAL_DISCOVERY_POSTS)
359
+ : MAX_SIGNAL_DISCOVERY_POSTS;
360
+ if (!normalizedTargetEngagers &&
361
+ !normalizedMaxPosts &&
362
+ posts.length <= MAX_SIGNAL_DISCOVERY_POSTS) {
357
363
  const availableEngagers = posts.reduce((sum, post) => sum + estimateScrapableSignalEngagers(post), 0);
358
364
  return {
359
365
  posts,
@@ -369,7 +375,7 @@ export function selectSignalPostsForImport(posts, options) {
369
375
  const selected = [];
370
376
  let estimatedEngagers = 0;
371
377
  for (const post of ranked) {
372
- if (normalizedMaxPosts && selected.length >= normalizedMaxPosts) {
378
+ if (selected.length >= effectiveMaxPosts) {
373
379
  break;
374
380
  }
375
381
  selected.push(post);
@@ -527,7 +533,7 @@ function buildSourceImportWatchNarration({ provider, selectedPostCount, estimate
527
533
  : ""}`
528
534
  : `the approved ${providerLabel} source`;
529
535
  const targetDetail = typeof targetLeadCount === "number"
530
- ? ` Targeting ${targetLeadCount.toLocaleString("en-US")} source leads before the first review sample is processed.`
536
+ ? ` Targeting ${targetLeadCount.toLocaleString("en-US")} source leads before campaign setup starts.`
531
537
  : "";
532
538
  return {
533
539
  stage: "find-leads",
@@ -536,8 +542,8 @@ function buildSourceImportWatchNarration({ provider, selectedPostCount, estimate
536
542
  : "Importing source leads",
537
543
  visibleState: `The browser is showing ${providerLabel} import progress for ${sourceDetail}.${targetDetail}`,
538
544
  agentIntent: "Codex is materializing the approved source into a lead list before copying the confirmed list into the campaign.",
539
- nextAction: "Wait for source leads, then confirm the list and review the first sample",
540
- safety: "This step materializes the source list; the first review sample is processed only after the later filter and message approvals.",
545
+ nextAction: "Wait for source leads, then confirm the list and configure campaign setup",
546
+ safety: "This step materializes the source list; the internal campaign-table execution slice is processed only after the later filter and message approvals.",
541
547
  progressLabel: "Source scouting",
542
548
  };
543
549
  }
@@ -550,11 +556,11 @@ function buildSourceImportRecoveryWatchNarration(args) {
550
556
  ? "Prospeo"
551
557
  : "source";
552
558
  const reasonCopy = args.reason === "failed"
553
- ? `${providerLabel} import failed before review rows were ready.`
559
+ ? `${providerLabel} import failed before campaign rows were ready.`
554
560
  : args.reason === "zero"
555
- ? `${providerLabel} import finished without usable review rows.`
561
+ ? `${providerLabel} import finished without usable campaign rows.`
556
562
  : args.reason === "timeout"
557
- ? `${providerLabel} import has not produced review rows within the wait window.`
563
+ ? `${providerLabel} import has not produced campaign rows within the wait window.`
558
564
  : `${providerLabel} import is still materializing source rows.`;
559
565
  return {
560
566
  stage: "review-batch",
@@ -771,10 +777,10 @@ function buildSignalDiscoverySourceRecommendation({ selectedPosts, }) {
771
777
 
772
778
  Use Signal Discovery first.
773
779
 
774
- **Goal:** ~${targetGoodFitLeads.toLocaleString("en-US")} good-fit prospects after cleanup, enrichment, and filters<br>
775
- **Working assumption:** ~${Math.round(defaultFitRate * 100)}% of raw post engagers become good-fit prospects<br>
780
+ **Goal:** ~${targetGoodFitLeads.toLocaleString("en-US")} headline-fit prospects that pass the Signal Discovery headline criteria<br>
781
+ **Working assumption:** ~${Math.round(defaultFitRate * 100)}% of raw post engagers pass headline filtering unless a real sample supports a different rate<br>
776
782
  **Engagers needed:** ~${sourceCandidateTarget.toLocaleString("en-US")} raw engagers<br>
777
- **Planning floor:** continue with Signal Discovery only when sampled/projected fit is at least ${Math.round(minPlanningFitRate * 100)}% after cleanup; below that, switch to Sales Nav recent activity<br>
783
+ **Planning floor:** continue with Signal Discovery only when sampled/projected headline-fit rate is at least ${Math.round(minPlanningFitRate * 100)}%; below that, switch to Sales Nav recent activity<br>
778
784
  **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
779
785
 
780
786
  ### Selected posts
@@ -784,17 +790,17 @@ Use Signal Discovery first.
784
790
  ${tableRows || "| Selected posts | Campaign-matched public engagement | - |"}
785
791
 
786
792
  **Total visible pool:** ${formatApproxInteger(totalEngagement)} engagers<br>
787
- **Estimated good-fit pool at ${Math.round(defaultFitRate * 100)}%:** ${formatApproxInteger(estimatedGoodFit)} prospects before dedupe/risk cleanup
793
+ **Estimated headline-fit pool at ${Math.round(defaultFitRate * 100)}%:** ${formatApproxInteger(estimatedGoodFit)} prospects before enrichment and deeper fit review
788
794
 
789
795
  ### Recommendation
790
796
 
791
797
  Approve scraping these ${selectedCount} posts.
792
798
 
793
- This gives enough volume to target ~${targetGoodFitLeads.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.
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.
794
800
 
795
- **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.
801
+ **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.
796
802
 
797
- **Fallback:** if the sampled/projected fit rate is below ${Math.round(minPlanningFitRate * 100)}%, or if the review batch is too vendor-heavy, agency-heavy, or off-ICP, switch to Sales Nav recent activity.
803
+ **Fallback:** if the sampled/projected headline-fit rate is below ${Math.round(minPlanningFitRate * 100)}%, or if the source sample is too vendor-heavy, agency-heavy, or off-ICP, switch to Sales Nav recent activity.
798
804
 
799
805
  Approval card should say:
800
806
 
@@ -1335,7 +1341,7 @@ export const leadToolDefinitions = [
1335
1341
  },
1336
1342
  {
1337
1343
  name: "import_leads",
1338
- description: "Create/select a source lead list and start the provider import/export job. Requires provider prompt preflight via get_provider_prompt for the active provider. For Sales Nav/Prospeo, targetLeadCount is the approved source-list export count computed from projected good-fit math, not the later 15-row review/process sample size. On success, this tool owns moving the watched campaign to confirm-lead-list with import/progress narration after a leadListId/jobId exists; do not call update_campaign to fix that step. After the user confirms the source list, call confirm_lead_list to copy the confirmed source rows into the campaign table and use only the first review/process sample for the initial flow.",
1344
+ description: "Create/select a source lead list and start the provider import/export job. Requires provider prompt preflight via get_provider_prompt for the active provider. For Sales Nav/Prospeo, targetLeadCount is the approved source-list export count computed from projected good-fit math, not the later internal campaign-table execution-slice size. On success, this tool owns moving the watched campaign to confirm-lead-list with import/progress narration after a leadListId/jobId exists; do not call update_campaign to fix that step. After the user confirms the source list, call confirm_lead_list to copy the confirmed source rows into the campaign table and use the initial campaign-table execution slice for setup.",
1339
1345
  inputSchema: {
1340
1346
  type: "object",
1341
1347
  properties: {
@@ -1362,7 +1368,7 @@ export const leadToolDefinitions = [
1362
1368
  },
1363
1369
  targetLeadCount: {
1364
1370
  type: "number",
1365
- description: "Provider source-list target count (max per provider from config). For Sales Nav/Prospeo this should be the export/materialization count needed to hit projected good-fit goals, not the 15-row review/process sample size.",
1371
+ description: "Provider source-list target count (max per provider from config). For Sales Nav/Prospeo this should be the export/materialization count needed to hit projected good-fit goals, not the internal campaign-table execution-slice size.",
1366
1372
  },
1367
1373
  mode: {
1368
1374
  type: "string",
@@ -1384,11 +1390,11 @@ export const leadToolDefinitions = [
1384
1390
  },
1385
1391
  targetEngagerCount: {
1386
1392
  type: "number",
1387
- description: "Signal Discovery: target number of post engagers/source candidates to scrape. Default planning target is about 300 good fits at a 20% raw-engager fit assumption, or about 1500 engagers. If the sampled/projected fit rate is below the 10% planning floor after cleanup, switch to the next provider instead of scaling noisy engagers. Limits selected posts before starting scrape.",
1393
+ description: "Signal Discovery: target number of post engagers/source candidates to scrape. Default planning target is about 300 headline-fit prospects at a 20% raw-engager headline-pass assumption, or about 1500 engagers. Use real sample math when available: target headline-fit prospects divided by sampled headline-pass rate. If the sampled/projected headline-fit rate is below the 10% planning floor, switch to the next provider instead of scaling noisy engagers. Limits selected posts before starting scrape, with a backend hard cap of 10 posts.",
1388
1394
  },
1389
1395
  maxPostsToScrape: {
1390
1396
  type: "number",
1391
- description: "Signal Discovery: optional hard cap on selected posts to scrape after ranking selected posts by engagement.",
1397
+ description: "Signal Discovery: optional hard cap on selected posts to scrape after ranking selected posts by engagement. Values above 10 are clamped to the backend hard cap.",
1392
1398
  },
1393
1399
  rubricGuidelines: {
1394
1400
  type: "array",
@@ -1428,7 +1434,7 @@ export const leadToolDefinitions = [
1428
1434
  },
1429
1435
  {
1430
1436
  name: "confirm_lead_list",
1431
- description: "After the user confirms the lead list looks good, copy the confirmed source list into the campaign table and mark the first review/process sample for the flow. This tool owns moving the watched campaign to filter-choice with sample-assessment narration only after non-empty campaign rows exist; do not call update_campaign to fix filter-choice afterward. selectedLeadListId remains the source lead list; workflowTableId is the campaign table.",
1437
+ description: "After the user confirms the lead list looks good, copy the confirmed source list into the campaign table and mark the initial campaign-table execution slice for the flow. This tool owns moving the watched campaign to filter-choice with campaign-setup narration only after non-empty campaign rows exist; do not call update_campaign to fix filter-choice afterward. selectedLeadListId remains the source lead list; workflowTableId is the campaign table.",
1432
1438
  inputSchema: {
1433
1439
  type: "object",
1434
1440
  properties: {
@@ -1484,7 +1490,7 @@ export const leadToolDefinitions = [
1484
1490
  },
1485
1491
  selections: {
1486
1492
  type: "array",
1487
- description: "Array of promising posts to select for scraping. Prefer the recommendedPostIds from search_signals.",
1493
+ description: "Array of promising posts to select for scraping. Prefer the recommendedPostIds from search_signals. Backend hard cap: 10 posts.",
1488
1494
  items: {
1489
1495
  type: "object",
1490
1496
  properties: {
@@ -1500,7 +1506,7 @@ export const leadToolDefinitions = [
1500
1506
  required: ["postId", "reason"],
1501
1507
  },
1502
1508
  minItems: 1,
1503
- maxItems: 20,
1509
+ maxItems: MAX_SIGNAL_DISCOVERY_POSTS,
1504
1510
  },
1505
1511
  headlineICPCriteria: {
1506
1512
  type: "array",
@@ -2468,6 +2474,9 @@ export async function importLeads(input) {
2468
2474
  }
2469
2475
  }
2470
2476
  const uniqueSelectedPosts = Array.from(uniqueByUrl.values());
2477
+ if (uniqueSelectedPosts.length > MAX_SIGNAL_DISCOVERY_POSTS) {
2478
+ throw new Error(`Maximum ${MAX_SIGNAL_DISCOVERY_POSTS} Signal Discovery posts can be imported for scraping. ${uniqueSelectedPosts.length} unique posts are currently selected; reduce the selected posts to the strongest ${MAX_SIGNAL_DISCOVERY_POSTS} before calling import_leads.`);
2479
+ }
2471
2480
  const importSelection = selectSignalPostsForImport(uniqueSelectedPosts, {
2472
2481
  targetEngagerCount: effectiveTargetEngagerCount,
2473
2482
  maxPostsToScrape,
@@ -2872,14 +2881,14 @@ export async function confirmLeadList(input) {
2872
2881
  remainingRowCount <= 0 &&
2873
2882
  importedRowCount > 0);
2874
2883
  if (!campaignTableReady) {
2875
- throw new Error("Campaign source rows are still copying and no review sample rows are available yet. Stay on lead import until the campaign table has rows, then retry confirm_lead_list or call wait_for_campaign_table_ready.");
2884
+ throw new Error("Campaign source rows are still copying and no campaign-table rows are available yet. Stay on lead import until the campaign table has rows, then retry confirm_lead_list or call wait_for_campaign_table_ready.");
2876
2885
  }
2877
2886
  if (importedRowCount <= 0) {
2878
2887
  const recoveryNarration = buildSourceImportRecoveryWatchNarration({
2879
2888
  reason: "zero",
2880
2889
  provider: resolvedProvider,
2881
2890
  });
2882
- throw new Error(`${recoveryNarration.headline}: No usable review rows were kept for the campaign table. Retry the import or change the approved source before continuing. ${recoveryNarration.safety}`);
2891
+ throw new Error(`${recoveryNarration.headline}: No usable campaign rows were kept for the campaign table. Retry the import or change the approved source before continuing. ${recoveryNarration.safety}`);
2883
2892
  }
2884
2893
  let reviewSampleRows = [];
2885
2894
  if (campaignTableId && effectiveCurrentStep === "filter-choice") {
@@ -2939,7 +2948,7 @@ export async function confirmLeadList(input) {
2939
2948
  "brief hash",
2940
2949
  "selectedLeadListId",
2941
2950
  "workflowTableId",
2942
- "first review/process sample row ids/hash",
2951
+ "initial campaign-table execution slice row ids/hash",
2943
2952
  "filter choice at branch start",
2944
2953
  ],
2945
2954
  promptRequired: 'Load get_subskill_prompt({ subskillName: "generate-messages", offset, limit }) until hasMore=false before drafting.',
@@ -3004,6 +3013,15 @@ export async function selectPromisingPosts(input) {
3004
3013
  const api = getApi();
3005
3014
  const { campaignOfferId, selections, headlineICPCriteria, selectionMode, mode, } = input;
3006
3015
  const effectiveMode = selectionMode ?? mode ?? "add";
3016
+ if (selections.length > MAX_SIGNAL_DISCOVERY_POSTS) {
3017
+ return {
3018
+ success: false,
3019
+ selectedCount: 0,
3020
+ unselectedCount: 0,
3021
+ criteriaCount: headlineICPCriteria.length,
3022
+ message: `Maximum ${MAX_SIGNAL_DISCOVERY_POSTS} Signal Discovery posts can be selected for scraping. Narrow the post set to the strongest ${MAX_SIGNAL_DISCOVERY_POSTS} before calling select_promising_posts again.`,
3023
+ };
3024
+ }
3007
3025
  // Update post selections via API
3008
3026
  const postIds = selections.map((s) => s.postId);
3009
3027
  let unselectedIds = [];
@@ -324,9 +324,9 @@ function describeVisibleStep(step) {
324
324
  case "leads":
325
325
  return {
326
326
  label: "Lead import",
327
- summary: "The lead preview or imported review batch is visible.",
327
+ summary: "The lead preview or imported campaign rows are visible.",
328
328
  nextMilestone: "Fit filtering and message review",
329
- guidance: "Confirm the browser-visible campaign state and row batch before filter/message work.",
329
+ guidance: "Confirm the browser-visible campaign state and imported rows before filter/message work.",
330
330
  checkpoint: "import",
331
331
  };
332
332
  case "filter-choice":
@@ -260,7 +260,7 @@ export function getPostFindLeadsScoutRegistry() {
260
260
  "campaignBrief",
261
261
  "source decision and selectedLeadList/source state",
262
262
  "workflowTableId",
263
- "first review/process sample rows from selectedLeadList with row ids/hash",
263
+ "initial campaign-table execution slice rows from selectedLeadList with row ids/hash",
264
264
  "filter choice and filter/rubric basis when present",
265
265
  ],
266
266
  scouts: scouts.map((agent) => ({
@@ -297,11 +297,11 @@ export function getPostFindLeadsScoutRegistry() {
297
297
  nextStage: "message-review",
298
298
  },
299
299
  messageDraftBranchContract: {
300
- firstAllowedStart: "after confirm_lead_list copies source rows and the first review/process sample exists",
300
+ firstAllowedStart: "after confirm_lead_list copies source rows and the initial campaign-table execution slice exists",
301
301
  forbiddenStarts: [
302
302
  "source recommendation",
303
303
  "provider import job alone",
304
- "zero-row review/process sample",
304
+ "zero-row campaign-table execution slice",
305
305
  ],
306
306
  runtimeProofTransport: "CampaignOffer.watchNarration.workerDetails.messageDraftBuilder",
307
307
  runtimeProofRequiredFields: [
@@ -321,7 +321,7 @@ export function getPostFindLeadsScoutRegistry() {
321
321
  "brief hash",
322
322
  "selectedLeadListId",
323
323
  "workflowTableId",
324
- "first review/process sample row ids/hash",
324
+ "initial campaign-table execution slice row ids/hash",
325
325
  "filter choice",
326
326
  "filter/rubric basis when present",
327
327
  ],
@@ -340,9 +340,9 @@ export function getPostFindLeadsScoutRegistry() {
340
340
  reusePolicy: "The first completed Message Draft Builder recommendation remains the default review candidate. Later Lead Fit Builder, Filter Leads, enrichment, or rubric completion may make an enriched rewrite available, but does not automatically retry or replace the initial draft unless campaign/brief/source/list/table/review-sample identity mismatches or the initial output failed.",
341
341
  },
342
342
  usage: {
343
- codex: "After confirm_lead_list copies source rows and the first review/process sample exists, ask the filter-choice question immediately. Do not spawn returned post-lead scout names before that question. Once the user answers, spawn Message Draft Builder from the same campaign/table basis. If the user chooses filters, also spawn Lead Fit Builder, move to Filter Rules, save rubrics, ask for filter approval, then keep the browser on Filter Leads while the message recommendation is reviewed. If filters are skipped, move to Messages/message review.",
344
- claude: "After confirm_lead_list copies source rows and the first review/process sample exists, ask the filter-choice question immediately. Do not invoke returned post-lead Task/Agent names before that question. Once the user answers, invoke Message Draft Builder from the same campaign/table basis. If the user chooses filters, also invoke Lead Fit Builder, move to Filter Rules, save rubrics, ask for filter approval, then keep the browser on Filter Leads while the message recommendation is reviewed. If filters are skipped, move to Messages/message review.",
345
- parentThreadRule: 'Named agents are optional acceleration, but message drafting is not optional. If post-find-leads-message-scout is available, run it as the background Message Draft Builder after the filter-choice answer. If it is absent, do not customer-surface install status; the main thread must execute the same message branch from CampaignOffer state, selected source state, workflowTableId, and first review/process sample rows. Local markdown/json files are not normal-path inputs. The filter-choice question is the first post-import user gate; do not load post-lead registries, filter references, or the full generate-messages prompt before it. Message drafting starts after the filter-choice answer, must load get_subskill_prompt({ subskillName: "generate-messages", offset, limit }) until hasMore=false, must read live campaign/review-sample state through scoped MCP/product tools, and must reject mismatched selectedLeadListId/workflowTableId/campaign/workspace input. On the filter path, keep the browser on Filter Rules after save_rubrics so the user can approve the saved criteria; only then move to Filter Leads and wait there for message approval. Enrichment, filtering, Generate Message cells, sender setup, sequence attach, and launch wait for template approval on the Use Template path. On the skip path, move to Messages/message review and wait for message approval before enrichment or Settings. Do not render message review from checklist or shortcut instructions; message review requires a messageDraftRecommendation whose basis proves the full generate-messages prompt ran for the current campaign/table/review sample. Do not automatically rerun Message Draft Builder after filters/enrichment finish; show the initial draft by default and offer an enriched rewrite only with explicit user opt-in.',
343
+ codex: "After confirm_lead_list copies source rows and the initial campaign-table execution slice exists, ask the filter-choice question immediately. Do not spawn returned post-lead scout names before that question. Once the user answers, spawn Message Draft Builder from the same campaign/table basis. If the user chooses filters, also spawn Lead Fit Builder, move to Filter Rules, save rubrics, ask for filter approval, then keep the browser on Filter Leads while the message recommendation is reviewed. If filters are skipped, move to Messages/message review.",
344
+ claude: "After confirm_lead_list copies source rows and the initial campaign-table execution slice exists, ask the filter-choice question immediately. Do not invoke returned post-lead Task/Agent names before that question. Once the user answers, invoke Message Draft Builder from the same campaign/table basis. If the user chooses filters, also invoke Lead Fit Builder, move to Filter Rules, save rubrics, ask for filter approval, then keep the browser on Filter Leads while the message recommendation is reviewed. If filters are skipped, move to Messages/message review.",
345
+ parentThreadRule: 'Named agents are optional acceleration, but message drafting is not optional. If post-find-leads-message-scout is available, run it as the background Message Draft Builder after the filter-choice answer. If it is absent, do not customer-surface install status; the main thread must execute the same message branch from CampaignOffer state, selected source state, workflowTableId, and initial campaign-table execution slice rows. Local markdown/json files are not normal-path inputs. The filter-choice question is the first post-import user gate; do not load post-lead registries, filter references, or the full generate-messages prompt before it. Message drafting starts after the filter-choice answer, must load get_subskill_prompt({ subskillName: "generate-messages", offset, limit }) until hasMore=false, must read live campaign table state through scoped MCP/product tools, and must reject mismatched selectedLeadListId/workflowTableId/campaign/workspace input. On the filter path, keep the browser on Filter Rules after save_rubrics so the user can approve the saved criteria; only then move to Filter Leads and wait there for message approval. Enrichment, filtering, Generate Message cells, sender setup, sequence attach, and launch wait for template approval on the Use Template path. On the skip path, move to Messages/message review and wait for message approval before enrichment or Settings. Do not render message review from checklist or shortcut instructions; message review requires a messageDraftRecommendation whose basis proves the full generate-messages prompt ran for the current campaign/table execution slice. Do not automatically rerun Message Draft Builder after filters/enrichment finish; show the initial draft by default and offer an enriched rewrite only with explicit user opt-in.',
346
346
  },
347
347
  };
348
348
  }
@@ -101,7 +101,7 @@ export const readinessToolDefinitions = [
101
101
  },
102
102
  targetLeadCount: {
103
103
  type: "number",
104
- description: "Target number of leads requested. Used as a fallback completion check when status is unavailable. For Signal Discovery, pass the approved source-candidate target; if the completed source list lands below it, the tool still returns ready with a source_shortfall warning so the confirmed list can be copied and the first review sample can proceed.",
104
+ description: "Target number of leads requested. Used as a fallback completion check when status is unavailable. For Signal Discovery, pass the approved source-candidate target; if the completed source list lands below it, the tool still returns ready with a source_shortfall warning so the confirmed list can be copied and campaign setup can proceed.",
105
105
  },
106
106
  timeoutMs: {
107
107
  type: "number",
@@ -489,7 +489,7 @@ export async function waitForLeadListReady(input) {
489
489
  targetLeadCount: targetLeadCount ?? null,
490
490
  sourceShortfall: signalSourceShortfall,
491
491
  warning: signalSourceShortfall
492
- ? `Signal Discovery completed with ${rowCount.toLocaleString("en-US")} source candidates, below the approved ${signalSourceShortfallTarget.toLocaleString("en-US")} source-candidate target. Confirm/copy the completed list for the first review sample, then add more approved posts or switch provider if the sample quality/scale is not enough.`
492
+ ? `Signal Discovery completed with ${rowCount.toLocaleString("en-US")} source candidates, below the approved ${signalSourceShortfallTarget.toLocaleString("en-US")} source-candidate target. Confirm/copy the completed list for campaign setup, then add more approved posts or switch provider if the source quality/scale is not enough.`
493
493
  : undefined,
494
494
  };
495
495
  }
@@ -99,7 +99,7 @@ const filterRulesSavedForReviewWatchNarration = {
99
99
  stage: "fit-message",
100
100
  headline: "Filter rules saved for review",
101
101
  visibleState: "The browser is showing Filter Rules with the saved criteria.",
102
- agentIntent: "Codex is waiting for filter approval before the review batch is enriched or scored.",
102
+ agentIntent: "Codex is waiting for filter approval before campaign rows are enriched or scored.",
103
103
  nextAction: "Approve or revise the filters",
104
104
  progressLabel: "Fit + message",
105
105
  safety: "Saved rules are ready to review; downstream row processing remains gated.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.154",
3
+ "version": "0.1.156",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",