@sellable/mcp 0.1.149 → 0.1.150

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
@@ -267,10 +267,14 @@ Parallel execution contract:
267
267
  Codex config, Claude agent files, and prompts should consume the registry.
268
268
  - Post-find-leads scout names also come from `agents/registry.json` and are
269
269
  exposed through `get_post_find_leads_scout_registry`. After
270
- `confirm_lead_list` imports a non-empty review batch and rows are proven, use
271
- that registry to launch the message-generation scout immediately when the host
272
- supports real subagents. Launch the filter-leads scout later, only after the
273
- user chooses filters.
270
+ `confirm_lead_list` imports a non-empty review batch and rows are proven, ask
271
+ the filter-choice question immediately. Do not load this registry or deep
272
+ filter/message prompts before that question. Once the user answers, launch the
273
+ message-generation scout from the same campaign/table basis. If the user
274
+ chooses filters, also launch the filter-leads scout, show Filter Rules, save
275
+ rubrics, then keep the browser on Filter Leads while the message
276
+ recommendation is reviewed. If filters are skipped, move to Messages/message
277
+ review. Product cells still wait for template approval.
274
278
  - Claude host: use the installed `source-scout-linkedin-engagement`,
275
279
  `source-scout-sales-nav`, and `source-scout-prospeo-contact` Task/Agent
276
280
  subagents for parallel lead-source scouting only when the current session
@@ -1,6 +1,6 @@
1
1
  You are the Prospeo Contact Scout for Sellable find-leads.
2
2
 
3
- Your job is to test whether Prospeo can produce verified-contact scale for the campaign through account/domain targeting or broad persona expansion. Work only on this source lane. Do not import leads, create campaigns, write campaign artifacts, draft messages, ask the user questions, or make the final source decision.
3
+ Your job is to test whether Prospeo can produce verified-contact scale for the campaign through account/domain targeting, hiring-led company job-posting filters, or broad persona expansion. Work only on this source lane. Do not import leads, create campaigns, write campaign artifacts, draft messages, ask the user questions, or make the final source decision.
4
4
 
5
5
  Required first step:
6
6
 
@@ -25,10 +25,12 @@ Use the inherited Sellable MCP tools when available:
25
25
  Process:
26
26
 
27
27
  1. Read the campaign brief, source intake, kickoff doc, or lane prompt supplied by the parent.
28
- 2. Identify whether this is domain/account targeting or broad persona expansion.
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
- 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, and require at least 10% projected good-fit after cleanup.
31
- 5. Call out that Prospeo gives contact/account coverage but usually weaker LinkedIn intent than LinkedIn Engagement or Sales Nav activity slices.
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 150 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 25-row import.
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
+ 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.
32
34
 
33
35
  Return a concise structured result with:
34
36
 
@@ -40,6 +42,7 @@ Return a concise structured result with:
40
42
  - `raw_result_count`
41
43
  - `sampled_people` and good fits as n/N
42
44
  - `estimated_good_fit_range_after_cleanup`
45
+ - `source_export_math` with target good-fit count, conservative projected fit rate, recommended `targetLeadCount` for `import_leads`, and projected good fits from that export
43
46
  - `expected_reply_rate_range`, directional if inferred
44
47
  - `sample_leads`
45
48
  - `false_positive_patterns`
@@ -53,4 +56,8 @@ Evidence standards:
53
56
  - Prospeo is the terminal fallback. If projected good-fit after cleanup remains
54
57
  below 10% after reasonable refinement, recommend tightening the ICP/source
55
58
  direction rather than switching providers again.
56
- - Treat Prospeo as an account/contact coverage lane, not as proof of fresh LinkedIn intent.
59
+ - Never recommend "import 25 leads" as the Prospeo source action. Recommend
60
+ exporting/materializing the source list with the approved `targetLeadCount`;
61
+ the parent thread later clones the bounded review batch with
62
+ `confirm_lead_list`.
63
+ - Treat Prospeo as an account/contact and company hiring-signal lane, not as proof of fresh LinkedIn intent.
@@ -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
- for Sales Nav, capped at 2,500 source candidates unless the parent supplies a target).
31
+ 5. Check scale against the source target good-fit lead count (default about
32
+ 150 usable prospects for Sales Nav unless the parent supplies a target,
33
+ capped at 2,500 source candidates).
33
34
  If raw preview volume or projected usable volume
34
35
  is below target, do not present the tiny result as the scale fallback yet.
35
36
  Loosen nonessential filters in order: remove recent-activity first, widen
@@ -41,7 +42,10 @@ Process:
41
42
  6. Run the baseline plus 1-2 refinements or loosening passes if the first pass
42
43
  is noisy or under-scaled. Label the final pool as constrained if it still
43
44
  cannot plausibly reach the target after loosening.
44
- 7. 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.
45
+ 7. Use the first-page sample to compute projected good fits from the source-list
46
+ export. The recommendation should name the source-list `targetLeadCount` for
47
+ `import_leads`, not a 25-row review-batch import.
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.
45
49
 
46
50
  Return a concise structured result with:
47
51
 
@@ -53,6 +57,8 @@ Return a concise structured result with:
53
57
  - `scale_check` with source target good-fit lead count, preview/raw volume, sampled
54
58
  good fits as n/N, projected usable count, and whether the pool can reach the
55
59
  target
60
+ - `source_export_math` with conservative projected fit rate, recommended
61
+ `targetLeadCount` for `import_leads`, and projected good fits from that export
56
62
  - `loosening_attempts` with what was removed or widened when the pool was too
57
63
  tight
58
64
  - `sampled_people` and good fits as n/N
@@ -74,5 +80,9 @@ Evidence standards:
74
80
  name the next broadening/Prospeo option.
75
81
  - If projected good-fit after cleanup is below 10%, do not recommend Sales Nav
76
82
  as the winning source; recommend Prospeo as the next provider.
83
+ - Never recommend "import 25 leads" as the Sales Nav source action. Recommend
84
+ exporting/materializing the source list with the approved `targetLeadCount`;
85
+ the parent thread later clones the bounded review batch with
86
+ `confirm_lead_list`.
77
87
  - Do not hand-wave missing filter IDs.
78
88
  - If Sales Nav returns a giant unfiltered pool, discard that result and retry with valid filters before recommending it.
@@ -1400,10 +1400,11 @@ export declare const leadToolDefinitions: ({
1400
1400
  items: {
1401
1401
  type: string;
1402
1402
  };
1403
+ description: string;
1403
1404
  };
1404
1405
  company_job_posting_quantity: {
1405
- type: string;
1406
1406
  description: string;
1407
+ type: string;
1407
1408
  properties: {
1408
1409
  min: {
1409
1410
  type: string;
@@ -8,6 +8,7 @@ import { buildCsvLinkedinPreview, matchesLinkedinConfirmationToken, parseLinkedi
8
8
  import { assertInteractionApproval } from "./interaction-mode.js";
9
9
  import { assertProviderPromptLoaded, markProviderPromptLoaded, } from "./provider-preflight.js";
10
10
  import { waitForLeadListReady } from "./readiness.js";
11
+ import { getTableRowsMinimal } from "./rows.js";
11
12
  const entryPath = process.argv[1] ? resolve(process.argv[1]) : process.cwd();
12
13
  const entryDir = dirname(entryPath);
13
14
  const workspaceRoot = resolveWorkspaceRoot(entryDir);
@@ -42,11 +43,11 @@ const defaultCampaignSourceDefaults = {
42
43
  postCoverageBuffer: 1.2,
43
44
  },
44
45
  "sales-nav": {
45
- targetGoodFitLeads: 300,
46
+ targetGoodFitLeads: 150,
46
47
  maxSourceCandidates: 2500,
47
48
  },
48
49
  prospeo: {
49
- targetGoodFitLeads: 300,
50
+ targetGoodFitLeads: 150,
50
51
  maxSourceCandidates: 2500,
51
52
  },
52
53
  },
@@ -525,8 +526,8 @@ function buildSourceImportWatchNarration({ provider, selectedPostCount, estimate
525
526
  : "Importing source leads",
526
527
  visibleState: `The browser is showing ${providerLabel} import progress for ${sourceDetail}.${targetDetail}`,
527
528
  agentIntent: "Codex is materializing the approved source into a lead list before cloning only the bounded review batch into the campaign.",
528
- nextAction: "Wait for source leads, then import the 15-row review batch",
529
- safety: "Import is limited to the review batch.",
529
+ nextAction: "Wait for source leads, then import the bounded review batch",
530
+ safety: "This step materializes the source list; only the bounded review batch is cloned into the campaign afterward.",
530
531
  progressLabel: "Source scouting",
531
532
  };
532
533
  }
@@ -558,19 +559,146 @@ function buildSourceImportRecoveryWatchNarration(args) {
558
559
  safety: "The campaign is still at source recovery.",
559
560
  };
560
561
  }
561
- function buildFilterChoiceWatchNarration({ sourceLeadCount, reviewRowCount, }) {
562
+ function normalizeLeadText(value) {
563
+ return typeof value === "string" ? value.trim().toLowerCase() : "";
564
+ }
565
+ function compactLeadText(value) {
566
+ return typeof value === "string" ? value.trim().replace(/\s+/g, " ") : "";
567
+ }
568
+ function rowSearchText(row) {
569
+ const carryValues = Object.values(row.carryData ?? {}).slice(0, 12);
570
+ return [row.name, row.title, row.company, ...carryValues]
571
+ .map(normalizeLeadText)
572
+ .filter(Boolean)
573
+ .join(" ");
574
+ }
575
+ function rowExample(row) {
576
+ const name = compactLeadText(row.name);
577
+ const title = compactLeadText(row.title);
578
+ const company = compactLeadText(row.company);
579
+ const label = name || company || title || "Unnamed lead";
580
+ return title ? `${label} (${title})` : label;
581
+ }
582
+ function collectExamples(rows, indexes) {
583
+ return indexes.slice(0, 2).map((index) => rowExample(rows[index]));
584
+ }
585
+ function formatExamples(prefix, examples) {
586
+ if (examples.length === 0)
587
+ return "";
588
+ const text = `${prefix} ${examples.join("; ")}.`;
589
+ return text.length > 180 ? `${text.slice(0, 177).trim()}...` : text;
590
+ }
591
+ function analyzeFilterChoiceSample(sampleRows) {
592
+ const rows = Array.isArray(sampleRows) ? sampleRows : [];
593
+ const strongIndexes = [];
594
+ const adjacentIndexes = [];
595
+ const riskIndexes = [];
596
+ const unknownIndexes = [];
597
+ const seniorPattern = /\b(founder|co[-\s]?founder|ceo|chief|cro|cmo|cco|vp|vice president|head of|director|owner|partner)\b/;
598
+ const gtmPattern = /\b(gtm|go[-\s]?to[-\s]?market|revenue|sales|growth|marketing|demand gen|demand generation|business development|bd\b|outbound|pipeline|revops|revenue operations|sales development|sdr)\b/;
599
+ const adjacentPattern = /\b(sales engineering|solutions? engineering|enablement|customer success|partnerships|partner|product marketing|marketing operations|sales operations)\b/;
600
+ const recruitingPattern = /\b(recruiter|recruiting|talent acquisition|people operations|human resources|\bhr\b)\b/;
601
+ const juniorOrNoisyPattern = /\b(intern|student|associate|coordinator|assistant|freelance|consultant|self-employed|open to work)\b/;
602
+ rows.forEach((row, index) => {
603
+ const text = rowSearchText(row);
604
+ if (!text) {
605
+ unknownIndexes.push(index);
606
+ return;
607
+ }
608
+ if (recruitingPattern.test(text) || juniorOrNoisyPattern.test(text)) {
609
+ riskIndexes.push(index);
610
+ return;
611
+ }
612
+ if (seniorPattern.test(text) && gtmPattern.test(text)) {
613
+ strongIndexes.push(index);
614
+ return;
615
+ }
616
+ if (seniorPattern.test(text) && adjacentPattern.test(text)) {
617
+ adjacentIndexes.push(index);
618
+ return;
619
+ }
620
+ unknownIndexes.push(index);
621
+ });
622
+ const sampleCount = rows.length;
623
+ if (sampleCount === 0) {
624
+ return {
625
+ sampleCount,
626
+ strongFitCount: 0,
627
+ adjacentCount: 0,
628
+ riskCount: 0,
629
+ unknownCount: 0,
630
+ strongExamples: [],
631
+ riskExamples: [],
632
+ recommendation: "manual-review",
633
+ };
634
+ }
635
+ const fitCount = strongIndexes.length + adjacentIndexes.length;
636
+ const riskCount = riskIndexes.length;
637
+ const unknownCount = unknownIndexes.length;
638
+ const cleanupCount = riskCount + unknownCount;
639
+ const fitRatio = fitCount / sampleCount;
640
+ const cleanupRatio = cleanupCount / sampleCount;
641
+ const adjacentRatio = adjacentIndexes.length / sampleCount;
642
+ const maxCleanRiskCount = Math.max(2, Math.floor(sampleCount * 0.12));
643
+ const recommendation = fitRatio >= 0.75 &&
644
+ cleanupCount <= maxCleanRiskCount &&
645
+ adjacentRatio <= 0.3
646
+ ? "skip"
647
+ : fitRatio >= 0.6 && cleanupRatio <= 0.3
648
+ ? "light-filter"
649
+ : "add-filter";
650
+ return {
651
+ sampleCount,
652
+ strongFitCount: strongIndexes.length,
653
+ adjacentCount: adjacentIndexes.length,
654
+ riskCount,
655
+ unknownCount,
656
+ strongExamples: collectExamples(rows, [
657
+ ...strongIndexes,
658
+ ...adjacentIndexes,
659
+ ]),
660
+ riskExamples: collectExamples(rows, [...riskIndexes, ...unknownIndexes]),
661
+ recommendation,
662
+ };
663
+ }
664
+ function buildFilterChoiceWatchNarration({ sourceLeadCount, reviewRowCount, sampleRows, }) {
562
665
  const sourceCopy = typeof sourceLeadCount === "number" && sourceLeadCount > 0
563
666
  ? ` from ${sourceLeadCount.toLocaleString("en-US")} source candidate${sourceLeadCount === 1 ? "" : "s"}`
564
667
  : "";
565
668
  const reviewCopy = typeof reviewRowCount === "number" && reviewRowCount > 0
566
669
  ? `${reviewRowCount.toLocaleString("en-US")} review lead${reviewRowCount === 1 ? " is" : "s are"} in the campaign table${sourceCopy}.`
567
670
  : `The bounded review batch is in the campaign table${sourceCopy}.`;
671
+ const assessment = analyzeFilterChoiceSample(sampleRows);
672
+ const fitCount = assessment.strongFitCount + assessment.adjacentCount;
673
+ const cleanupCount = assessment.riskCount + assessment.unknownCount;
674
+ const sampleCopy = assessment.sampleCount > 0
675
+ ? `Sample read: ${fitCount}/${assessment.sampleCount} look like senior target-role fits; ${cleanupCount} need cleanup or manual review.`
676
+ : "Sample rows were not available to inspect, so this is a manual filter decision.";
677
+ const fitExamples = formatExamples("Fit examples:", assessment.strongExamples);
678
+ const riskExamples = formatExamples("Cleanup examples:", assessment.riskExamples);
679
+ const headline = assessment.recommendation === "skip"
680
+ ? "Filtering may not be needed"
681
+ : assessment.recommendation === "light-filter"
682
+ ? "I recommend light cleanup filters"
683
+ : assessment.recommendation === "manual-review"
684
+ ? "Review filters or skip"
685
+ : "I recommend adding filters";
686
+ const agentIntent = assessment.recommendation === "skip"
687
+ ? `Codex is not recommending filters because the sample already looks clean. ${fitExamples} Add filters only for a narrow exclusion.`
688
+ : assessment.recommendation === "light-filter"
689
+ ? `Codex is recommending light filters because most rows fit, but ${cleanupCount} of ${assessment.sampleCount} look adjacent, unknown, or risky. ${riskExamples}`
690
+ : assessment.recommendation === "manual-review"
691
+ ? "Codex could not inspect the review rows, so it is pausing for a manual filter decision instead of assuming filters are required."
692
+ : `Codex is recommending filters because ${cleanupCount} of ${assessment.sampleCount} sampled rows look adjacent, unknown, or risky before message review. ${riskExamples}`;
693
+ const nextAction = assessment.recommendation === "skip"
694
+ ? "Skip filters or add a narrow cleanup rule"
695
+ : "Choose filters or skip";
568
696
  return {
569
697
  stage: "fit-message",
570
- headline: "I recommend adding filters",
571
- visibleState: `${reviewCopy} The browser is showing the filter-choice screen and sample rows.`,
572
- agentIntent: "Codex is pausing on this sample because a mixed review list should be filtered before message review. Skip filters only if the visible rows already look clean.",
573
- nextAction: "Choose filters or skip",
698
+ headline,
699
+ visibleState: `${reviewCopy} ${sampleCopy} The browser is showing the filter-choice screen.`,
700
+ agentIntent: agentIntent.trim(),
701
+ nextAction,
574
702
  safety: "This choice sets the filter path.",
575
703
  workerStatuses: {
576
704
  leadFitBuilder: "idle",
@@ -1011,7 +1139,7 @@ export const leadToolDefinitions = [
1011
1139
  },
1012
1140
  {
1013
1141
  name: "search_prospeo",
1014
- description: 'Search Prospeo for people using filters and optional domainFilterId. Requires get_provider_prompt({ provider: "prospeo" }) first. When targeting known accounts, call load_csv_domains for CSV-on-disk workflows or save_domain_filters for pasted/raw domain lists, then pass domainFilterId. Raw domain inputs and company-name targeting are NOT supported in this MCP tool. Strategy: start with 2-3 high-signal filters (title/seniority + industry or domainFilterId + headcount), then tighten one filter at a time. For security, AppSec, SOC, RevOps, Demand Gen, and similar function-specific lanes, do not widen with bare seniority labels like "Head" or "Director" alone; pair them with explicit function-title keywords and inspect the sample for off-function `Head of X` leakage. Prefer person location over company HQ unless HQ is explicitly needed. `campaignOfferId` routing rule: OMIT campaignOfferId ONLY in pre-mint Phase 84 `find leads` discovery mode (validating ICP before the commit gate). In every other context — post-mint lead additions, operator-driven searches on a live campaign, any search where the intent is to persist results to a specific campaign — you MUST pass campaignOfferId so the search shows up in that campaign\'s Contact Search panel. Post-mint create-campaign-v2 watch runs MUST pass currentStep: "prospeo". Omitting campaignOfferId post-mint orphans the search from the UI. Returns normalized results with pagination.',
1142
+ description: 'Search Prospeo for people using filters and optional domainFilterId. Requires get_provider_prompt({ provider: "prospeo" }) first. Use Prospeo first for hiring-led targeting because it supports company_job_posting_hiring_for and company_job_posting_quantity; Sales Nav does not filter companies by hiring role. When targeting known accounts, call load_csv_domains for CSV-on-disk workflows or save_domain_filters for pasted/raw domain lists, then pass domainFilterId. Raw domain inputs and company-name targeting are NOT supported in this MCP tool. Strategy: start with 2-3 high-signal filters (title/seniority + industry or domainFilterId + headcount), add job-posting filters for hiring-led campaigns, then tighten one filter at a time. For security, AppSec, SOC, RevOps, Demand Gen, and similar function-specific lanes, do not widen with bare seniority labels like "Head" or "Director" alone; pair them with explicit function-title keywords and inspect the sample for off-function `Head of X` leakage. Prefer person location over company HQ unless HQ is explicitly needed. `campaignOfferId` routing rule: OMIT campaignOfferId ONLY in pre-mint Phase 84 `find leads` discovery mode (validating ICP before the commit gate). In every other context — post-mint lead additions, operator-driven searches on a live campaign, any search where the intent is to persist results to a specific campaign — you MUST pass campaignOfferId so the search shows up in that campaign\'s Contact Search panel. Post-mint create-campaign-v2 watch runs MUST pass currentStep: "prospeo". Omitting campaignOfferId post-mint orphans the search from the UI. Returns normalized results with pagination.',
1015
1143
  inputSchema: {
1016
1144
  type: "object",
1017
1145
  properties: {
@@ -1078,8 +1206,12 @@ export const leadToolDefinitions = [
1078
1206
  company_job_posting_hiring_for: {
1079
1207
  type: "array",
1080
1208
  items: { type: "string" },
1209
+ description: "Company hiring filter. Use for hiring-led campaigns; values are open-role themes such as SDR, Account Executive, Growth, RevOps, Demand Generation, GTM Engineer, or AI Engineer.",
1210
+ },
1211
+ company_job_posting_quantity: {
1212
+ ...prospeoRangeFilterSchema,
1213
+ description: "Active job posting count range. Use min when the campaign needs companies with a real hiring push.",
1081
1214
  },
1082
- company_job_posting_quantity: prospeoRangeFilterSchema,
1083
1215
  company_headcount_by_department: {
1084
1216
  type: "array",
1085
1217
  items: prospeoCompanyHeadcountByDepartmentSchema,
@@ -1192,7 +1324,7 @@ export const leadToolDefinitions = [
1192
1324
  },
1193
1325
  {
1194
1326
  name: "import_leads",
1195
- description: "Create/select a lead list and start the provider import job. Requires provider prompt preflight via get_provider_prompt for the active provider. 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 list, call confirm_lead_list.",
1327
+ 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 bounded review-batch 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 clone only the bounded review batch into the campaign table.",
1196
1328
  inputSchema: {
1197
1329
  type: "object",
1198
1330
  properties: {
@@ -1219,7 +1351,7 @@ export const leadToolDefinitions = [
1219
1351
  },
1220
1352
  targetLeadCount: {
1221
1353
  type: "number",
1222
- description: "Target number of leads (max per provider from config)",
1354
+ 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 25-row review-batch size.",
1223
1355
  },
1224
1356
  mode: {
1225
1357
  type: "string",
@@ -2741,6 +2873,20 @@ export async function confirmLeadList(input) {
2741
2873
  await Promise.all(batch.map((rowId) => api.delete(`/api/v3/workflow-tables/${campaignTableId}/rows/${rowId}`)));
2742
2874
  }
2743
2875
  }
2876
+ let reviewSampleRows = [];
2877
+ if (campaignTableId && effectiveCurrentStep === "filter-choice") {
2878
+ try {
2879
+ const sampleLimit = Math.min(Math.max(keptReviewRowCount, 1), 25);
2880
+ const sample = await getTableRowsMinimal(campaignTableId, {
2881
+ limit: sampleLimit,
2882
+ page: 1,
2883
+ });
2884
+ reviewSampleRows = Array.isArray(sample?.rows) ? sample.rows : [];
2885
+ }
2886
+ catch {
2887
+ reviewSampleRows = [];
2888
+ }
2889
+ }
2744
2890
  // Persist currentStep if the caller asked for it. Do NOT touch
2745
2891
  // selectedLeadListId here: the campaign table id is already saved to
2746
2892
  // CampaignOffer.workflowTableId by /api/v3/campaign-builder/import-leads,
@@ -2758,6 +2904,7 @@ export async function confirmLeadList(input) {
2758
2904
  watchNarration: buildFilterChoiceWatchNarration({
2759
2905
  sourceLeadCount: leadListRowCount,
2760
2906
  reviewRowCount: keptReviewRowCount,
2907
+ sampleRows: reviewSampleRows,
2761
2908
  }),
2762
2909
  }
2763
2910
  : {}),
@@ -2800,8 +2947,8 @@ export async function confirmLeadList(input) {
2800
2947
  : undefined,
2801
2948
  message: requestedTargetLeadCount !== null &&
2802
2949
  leadListRowCount > requestedTargetLeadCount
2803
- ? `I found ${leadListRowCount} source candidates and imported the first ${Math.min(importedRowCount, requestedTargetLeadCount)} into the campaign review table. The watched campaign is now on filter-choice with sample assessment copy; announce Message Draft Builder background start only if that branch has actually started.`
2804
- : "Lead list imported into the campaign review table. The watched campaign is now on filter-choice with sample assessment copy; announce Message Draft Builder background start only if that branch has actually started.",
2950
+ ? `I found ${leadListRowCount} source candidates and imported the first ${Math.min(importedRowCount, requestedTargetLeadCount)} into the campaign review table. The watched campaign is now on filter-choice; ask add filters vs skip filters before loading post-lead workers.`
2951
+ : "Lead list imported into the campaign review table. The watched campaign is now on filter-choice; ask add filters vs skip filters before loading post-lead workers.",
2805
2952
  };
2806
2953
  }
2807
2954
  export function getProviderPrompt(input) {
@@ -339,9 +339,9 @@ export function getPostFindLeadsScoutRegistry() {
339
339
  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-batch identity mismatches or the initial output failed.",
340
340
  },
341
341
  usage: {
342
- codex: "After confirm_lead_list imports a non-empty bounded review batch and get_rows_minimal proves rows for workflowTableId, spawn the returned Message Draft Builder scout name immediately when the current Codex host exposes that custom agent. Spawn the Lead Fit Builder scout only after the user chooses filters.",
343
- claude: "After confirm_lead_list imports a non-empty bounded review batch and get_rows_minimal proves rows for workflowTableId, invoke the returned Message Draft Builder Task/Agent immediately when the current Claude session lists that agent. Invoke the Lead Fit Builder only after the user chooses filters.",
344
- parentThreadRule: "Named agents are optional acceleration. If they are absent, do not customer-surface install status; the main thread still orchestrates filter and message branches from CampaignOffer state, selected source state, workflowTableId, and imported review-batch rows. Local markdown/json files are not normal-path inputs. The message drafting branch starts at the review-batch import gate, must load the full generate-messages prompt, must read live campaign/review-batch state through scoped MCP/product tools, and must reject mismatched selectedLeadListId/workflowTableId/campaign/workspace input. Template approval in the parent thread uses the create-campaign-v2 message-review safety gate; do not load the full generate-messages prompt for approval-only review. Join before message review. 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.",
342
+ codex: "After confirm_lead_list imports a non-empty bounded review batch and get_rows_minimal proves rows for workflowTableId, 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, then keep the browser on Filter Leads while the message recommendation is reviewed. If filters are skipped, move to Messages/message review.",
343
+ claude: "After confirm_lead_list imports a non-empty bounded review batch and get_rows_minimal proves rows for workflowTableId, 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, then keep the browser on Filter Leads while the message recommendation is reviewed. If filters are skipped, move to Messages/message review.",
344
+ parentThreadRule: "Named agents are optional acceleration. If they are absent, do not customer-surface install status; the main thread still orchestrates filter and message branches from CampaignOffer state, selected source state, workflowTableId, and imported review-batch 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 the full generate-messages prompt, must read live campaign/review-batch 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 then Filter Leads while the background message agent prepares the template; enrichment/filtering/Generate Message cells wait for template approval. On the skip path, move to Messages/message review and wait for template approval before enrichment. Template approval in the parent thread uses the create-campaign-v2 message-review safety gate; do not load the full generate-messages prompt for approval-only review. 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.",
345
345
  },
346
346
  };
347
347
  }
@@ -2557,10 +2557,11 @@ export declare const allTools: ({
2557
2557
  items: {
2558
2558
  type: string;
2559
2559
  };
2560
+ description: string;
2560
2561
  };
2561
2562
  company_job_posting_quantity: {
2562
- type: string;
2563
2563
  description: string;
2564
+ type: string;
2564
2565
  properties: {
2565
2566
  min: {
2566
2567
  type: string;
@@ -3702,6 +3703,7 @@ export declare const allTools: ({
3702
3703
  tableId?: undefined;
3703
3704
  targetCount?: undefined;
3704
3705
  minPassedCount?: undefined;
3706
+ minMessagesCount?: undefined;
3705
3707
  timeoutMs?: undefined;
3706
3708
  intervalMs?: undefined;
3707
3709
  includeRows?: undefined;
@@ -3735,6 +3737,7 @@ export declare const allTools: ({
3735
3737
  tableId?: undefined;
3736
3738
  targetCount?: undefined;
3737
3739
  minPassedCount?: undefined;
3740
+ minMessagesCount?: undefined;
3738
3741
  timeoutMs?: undefined;
3739
3742
  intervalMs?: undefined;
3740
3743
  includeRows?: undefined;
@@ -3793,6 +3796,7 @@ export declare const allTools: ({
3793
3796
  tableId?: undefined;
3794
3797
  targetCount?: undefined;
3795
3798
  minPassedCount?: undefined;
3799
+ minMessagesCount?: undefined;
3796
3800
  timeoutMs?: undefined;
3797
3801
  intervalMs?: undefined;
3798
3802
  includeRows?: undefined;
@@ -3851,6 +3855,7 @@ export declare const allTools: ({
3851
3855
  tableId?: undefined;
3852
3856
  targetCount?: undefined;
3853
3857
  minPassedCount?: undefined;
3858
+ minMessagesCount?: undefined;
3854
3859
  timeoutMs?: undefined;
3855
3860
  intervalMs?: undefined;
3856
3861
  includeRows?: undefined;
@@ -3911,6 +3916,7 @@ export declare const allTools: ({
3911
3916
  tableId?: undefined;
3912
3917
  targetCount?: undefined;
3913
3918
  minPassedCount?: undefined;
3919
+ minMessagesCount?: undefined;
3914
3920
  timeoutMs?: undefined;
3915
3921
  intervalMs?: undefined;
3916
3922
  includeRows?: undefined;
@@ -3941,6 +3947,7 @@ export declare const allTools: ({
3941
3947
  tableId?: undefined;
3942
3948
  targetCount?: undefined;
3943
3949
  minPassedCount?: undefined;
3950
+ minMessagesCount?: undefined;
3944
3951
  timeoutMs?: undefined;
3945
3952
  intervalMs?: undefined;
3946
3953
  includeRows?: undefined;
@@ -3971,6 +3978,7 @@ export declare const allTools: ({
3971
3978
  tableId?: undefined;
3972
3979
  targetCount?: undefined;
3973
3980
  minPassedCount?: undefined;
3981
+ minMessagesCount?: undefined;
3974
3982
  timeoutMs?: undefined;
3975
3983
  intervalMs?: undefined;
3976
3984
  includeRows?: undefined;
@@ -120,6 +120,7 @@ export declare function getTableRows(tableId: string, options?: {
120
120
  export declare function getTableRowsMinimal(tableId: string, options?: {
121
121
  limit?: number;
122
122
  page?: number;
123
+ includeMessages?: boolean;
123
124
  }): Promise<RowsResponse>;
124
125
  export declare function getRows(tableId: string, rowIds: string | string[]): Promise<{
125
126
  rows: FullRow[];
@@ -90,6 +90,8 @@ export async function getTableRowsMinimal(tableId, options) {
90
90
  if (options?.page)
91
91
  params.set("page", String(options.page));
92
92
  params.set("minimal", "1");
93
+ if (options?.includeMessages)
94
+ params.set("includeMessages", "1");
93
95
  const queryString = params.toString();
94
96
  const path = `/api/v3/mcp/tables/${tableId}/rows${queryString ? `?${queryString}` : ""}`;
95
97
  return api.get(path);