@sellable/mcp 0.1.153 → 0.1.155

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 (32) 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 +3 -3
  4. package/agents/source-scout-prospeo-contact.md +3 -3
  5. package/agents/source-scout-sales-nav.md +3 -3
  6. package/dist/tools/cells.js +1 -1
  7. package/dist/tools/leads.d.ts +1 -0
  8. package/dist/tools/leads.js +51 -24
  9. package/dist/tools/navigation.js +2 -2
  10. package/dist/tools/prompts.js +7 -7
  11. package/dist/tools/readiness.js +2 -2
  12. package/dist/tools/rubrics.js +1 -1
  13. package/package.json +1 -1
  14. package/skills/create-campaign/SKILL.md +52 -21
  15. package/skills/create-campaign-v2/SKILL.md +48 -47
  16. package/skills/create-campaign-v2/SOUL.md +4 -4
  17. package/skills/create-campaign-v2/core/auto-execute.README.md +9 -9
  18. package/skills/create-campaign-v2/core/flow.v2.json +84 -19
  19. package/skills/create-campaign-v2/core/policy.md +1 -1
  20. package/skills/create-campaign-v2/references/approval-gate-framing.md +2 -2
  21. package/skills/create-campaign-v2/references/filter-leads.md +11 -11
  22. package/skills/create-campaign-v2/references/final-handoff-contract.md +4 -4
  23. package/skills/create-campaign-v2/references/sample-validation-loop.md +8 -8
  24. package/skills/create-campaign-v2/references/step-13-import-leads.md +9 -9
  25. package/skills/create-campaign-v2/references/step-15-re-cascade.md +2 -2
  26. package/skills/create-campaign-v2/references/watch-guide-narration.md +15 -15
  27. package/skills/create-campaign-v2-tail/SKILL.md +27 -27
  28. package/skills/create-rubric/SKILL.md +1 -1
  29. package/skills/find-leads/SKILL.md +2 -2
  30. package/skills/providers/prospeo.md +1 -1
  31. package/skills/providers/sales-nav.md +1 -1
  32. package/skills/providers/signal-discovery.md +7 -7
@@ -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
@@ -76,7 +76,7 @@ Return a concise structured result with:
76
76
  100 engagers, required engagers to scrape, average reachable engagers per
77
77
  post, expected usable prospects per post after cleanup, posts needed for
78
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
79
+ count, internal campaign-table execution-slice size, expected usable lead range, and scale
80
80
  fallback
81
81
  - `estimated_good_fit_range`
82
82
  - `message_context_strength`, directional and source-specific
@@ -98,8 +98,8 @@ Evidence standards:
98
98
  post should yield after cleanup, how many engagers must be scraped for the
99
99
  300-good-fit source target at the 20% working assumption, how many posts are
100
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.
101
+ source list is copied into the campaign and only the first campaign-table
102
+ execution slice is processed internally for filter and message setup.
103
103
  - If `fetch_post_engagers` is unavailable or fails, report that explicitly and mark the estimate lower-confidence.
104
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
105
  - If sampled/projected fit after cleanup is below 10%, reject the Signals scrape
@@ -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.
@@ -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,12 @@ const defaultCampaignSourceDefaults = {
53
53
  },
54
54
  };
55
55
  const defaultProviderSourceListTarget = 1000;
56
+ export const MAX_SIGNAL_DISCOVERY_POSTS = 10;
57
+ // Mirror the web app's Signal Discovery Harvest caps. The MCP tool selects and
58
+ // describes the scrape before the web API runs, so it must use the same capped
59
+ // capacity math instead of raw visible engagement counts.
60
+ const signalDiscoveryReactionFetchLimit = 1000;
61
+ const signalDiscoveryCommentFetchLimit = 1000;
56
62
  const prospeoFilterValueSchema = {
57
63
  type: "object",
58
64
  description: "Include/exclude list filter (values must match Prospeo enums)",
@@ -341,11 +347,20 @@ function normalizePositiveInteger(value) {
341
347
  }
342
348
  return Math.floor(numeric);
343
349
  }
350
+ function estimateScrapableSignalEngagers(post) {
351
+ return (Math.min(post.likes, signalDiscoveryReactionFetchLimit) +
352
+ Math.min(post.comments, signalDiscoveryCommentFetchLimit));
353
+ }
344
354
  export function selectSignalPostsForImport(posts, options) {
345
355
  const normalizedTargetEngagers = normalizePositiveInteger(options.targetEngagerCount);
346
356
  const normalizedMaxPosts = normalizePositiveInteger(options.maxPostsToScrape);
347
- if (!normalizedTargetEngagers && !normalizedMaxPosts) {
348
- const availableEngagers = posts.reduce((sum, post) => sum + post.likes + post.comments, 0);
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) {
363
+ const availableEngagers = posts.reduce((sum, post) => sum + estimateScrapableSignalEngagers(post), 0);
349
364
  return {
350
365
  posts,
351
366
  estimatedEngagers: availableEngagers,
@@ -355,16 +370,16 @@ export function selectSignalPostsForImport(posts, options) {
355
370
  limited: false,
356
371
  };
357
372
  }
358
- const ranked = [...posts].sort((a, b) => b.likes + b.comments - (a.likes + a.comments));
359
- const availableEngagers = ranked.reduce((sum, post) => sum + post.likes + post.comments, 0);
373
+ const ranked = [...posts].sort((a, b) => estimateScrapableSignalEngagers(b) - estimateScrapableSignalEngagers(a));
374
+ const availableEngagers = ranked.reduce((sum, post) => sum + estimateScrapableSignalEngagers(post), 0);
360
375
  const selected = [];
361
376
  let estimatedEngagers = 0;
362
377
  for (const post of ranked) {
363
- if (normalizedMaxPosts && selected.length >= normalizedMaxPosts) {
378
+ if (selected.length >= effectiveMaxPosts) {
364
379
  break;
365
380
  }
366
381
  selected.push(post);
367
- estimatedEngagers += post.likes + post.comments;
382
+ estimatedEngagers += estimateScrapableSignalEngagers(post);
368
383
  if (normalizedTargetEngagers &&
369
384
  estimatedEngagers >= normalizedTargetEngagers) {
370
385
  break;
@@ -518,7 +533,7 @@ function buildSourceImportWatchNarration({ provider, selectedPostCount, estimate
518
533
  : ""}`
519
534
  : `the approved ${providerLabel} source`;
520
535
  const targetDetail = typeof targetLeadCount === "number"
521
- ? ` 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.`
522
537
  : "";
523
538
  return {
524
539
  stage: "find-leads",
@@ -527,8 +542,8 @@ function buildSourceImportWatchNarration({ provider, selectedPostCount, estimate
527
542
  : "Importing source leads",
528
543
  visibleState: `The browser is showing ${providerLabel} import progress for ${sourceDetail}.${targetDetail}`,
529
544
  agentIntent: "Codex is materializing the approved source into a lead list before copying the confirmed list into the campaign.",
530
- nextAction: "Wait for source leads, then confirm the list and review the first sample",
531
- 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.",
532
547
  progressLabel: "Source scouting",
533
548
  };
534
549
  }
@@ -541,11 +556,11 @@ function buildSourceImportRecoveryWatchNarration(args) {
541
556
  ? "Prospeo"
542
557
  : "source";
543
558
  const reasonCopy = args.reason === "failed"
544
- ? `${providerLabel} import failed before review rows were ready.`
559
+ ? `${providerLabel} import failed before campaign rows were ready.`
545
560
  : args.reason === "zero"
546
- ? `${providerLabel} import finished without usable review rows.`
561
+ ? `${providerLabel} import finished without usable campaign rows.`
547
562
  : args.reason === "timeout"
548
- ? `${providerLabel} import has not produced review rows within the wait window.`
563
+ ? `${providerLabel} import has not produced campaign rows within the wait window.`
549
564
  : `${providerLabel} import is still materializing source rows.`;
550
565
  return {
551
566
  stage: "review-batch",
@@ -783,9 +798,9 @@ Approve scraping these ${selectedCount} posts.
783
798
 
784
799
  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.
785
800
 
786
- **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.
787
802
 
788
- **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 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.
789
804
 
790
805
  Approval card should say:
791
806
 
@@ -1326,7 +1341,7 @@ export const leadToolDefinitions = [
1326
1341
  },
1327
1342
  {
1328
1343
  name: "import_leads",
1329
- 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.",
1330
1345
  inputSchema: {
1331
1346
  type: "object",
1332
1347
  properties: {
@@ -1353,7 +1368,7 @@ export const leadToolDefinitions = [
1353
1368
  },
1354
1369
  targetLeadCount: {
1355
1370
  type: "number",
1356
- 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.",
1357
1372
  },
1358
1373
  mode: {
1359
1374
  type: "string",
@@ -1375,11 +1390,11 @@ export const leadToolDefinitions = [
1375
1390
  },
1376
1391
  targetEngagerCount: {
1377
1392
  type: "number",
1378
- 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 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, with a backend hard cap of 10 posts.",
1379
1394
  },
1380
1395
  maxPostsToScrape: {
1381
1396
  type: "number",
1382
- 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.",
1383
1398
  },
1384
1399
  rubricGuidelines: {
1385
1400
  type: "array",
@@ -1419,7 +1434,7 @@ export const leadToolDefinitions = [
1419
1434
  },
1420
1435
  {
1421
1436
  name: "confirm_lead_list",
1422
- 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.",
1423
1438
  inputSchema: {
1424
1439
  type: "object",
1425
1440
  properties: {
@@ -1475,7 +1490,7 @@ export const leadToolDefinitions = [
1475
1490
  },
1476
1491
  selections: {
1477
1492
  type: "array",
1478
- 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.",
1479
1494
  items: {
1480
1495
  type: "object",
1481
1496
  properties: {
@@ -1491,7 +1506,7 @@ export const leadToolDefinitions = [
1491
1506
  required: ["postId", "reason"],
1492
1507
  },
1493
1508
  minItems: 1,
1494
- maxItems: 20,
1509
+ maxItems: MAX_SIGNAL_DISCOVERY_POSTS,
1495
1510
  },
1496
1511
  headlineICPCriteria: {
1497
1512
  type: "array",
@@ -2459,6 +2474,9 @@ export async function importLeads(input) {
2459
2474
  }
2460
2475
  }
2461
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
+ }
2462
2480
  const importSelection = selectSignalPostsForImport(uniqueSelectedPosts, {
2463
2481
  targetEngagerCount: effectiveTargetEngagerCount,
2464
2482
  maxPostsToScrape,
@@ -2863,14 +2881,14 @@ export async function confirmLeadList(input) {
2863
2881
  remainingRowCount <= 0 &&
2864
2882
  importedRowCount > 0);
2865
2883
  if (!campaignTableReady) {
2866
- 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.");
2867
2885
  }
2868
2886
  if (importedRowCount <= 0) {
2869
2887
  const recoveryNarration = buildSourceImportRecoveryWatchNarration({
2870
2888
  reason: "zero",
2871
2889
  provider: resolvedProvider,
2872
2890
  });
2873
- 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}`);
2874
2892
  }
2875
2893
  let reviewSampleRows = [];
2876
2894
  if (campaignTableId && effectiveCurrentStep === "filter-choice") {
@@ -2930,7 +2948,7 @@ export async function confirmLeadList(input) {
2930
2948
  "brief hash",
2931
2949
  "selectedLeadListId",
2932
2950
  "workflowTableId",
2933
- "first review/process sample row ids/hash",
2951
+ "initial campaign-table execution slice row ids/hash",
2934
2952
  "filter choice at branch start",
2935
2953
  ],
2936
2954
  promptRequired: 'Load get_subskill_prompt({ subskillName: "generate-messages", offset, limit }) until hasMore=false before drafting.',
@@ -2995,6 +3013,15 @@ export async function selectPromisingPosts(input) {
2995
3013
  const api = getApi();
2996
3014
  const { campaignOfferId, selections, headlineICPCriteria, selectionMode, mode, } = input;
2997
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
+ }
2998
3025
  // Update post selections via API
2999
3026
  const postIds = selections.map((s) => s.postId);
3000
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.153",
3
+ "version": "0.1.155",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",