@sellable/mcp 0.1.151 → 0.1.152

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/README.md +4 -3
  2. package/agents/post-find-leads-filter-scout.md +5 -4
  3. package/agents/post-find-leads-message-scout.md +15 -14
  4. package/agents/source-scout-linkedin-engagement.md +6 -5
  5. package/agents/source-scout-prospeo-contact.md +4 -4
  6. package/agents/source-scout-sales-nav.md +4 -4
  7. package/dist/index-dev.js +0 -0
  8. package/dist/index.js +0 -0
  9. package/dist/tools/cells.js +1 -1
  10. package/dist/tools/leads.d.ts +36 -3
  11. package/dist/tools/leads.js +83 -71
  12. package/dist/tools/prompts.js +9 -9
  13. package/dist/tools/registry.d.ts +17 -0
  14. package/dist/tools/rubrics.js +23 -20
  15. package/package.json +1 -1
  16. package/skills/create-campaign/SKILL.md +59 -56
  17. package/skills/create-campaign-v2/SKILL.md +43 -42
  18. package/skills/create-campaign-v2/SOUL.md +16 -13
  19. package/skills/create-campaign-v2/core/auto-execute.README.md +16 -17
  20. package/skills/create-campaign-v2/core/auto-execute.yaml +8 -7
  21. package/skills/create-campaign-v2/core/flow.v2.json +81 -149
  22. package/skills/create-campaign-v2/core/policy.md +13 -12
  23. package/skills/create-campaign-v2/references/approval-gate-framing.md +4 -3
  24. package/skills/create-campaign-v2/references/filter-leads.md +5 -4
  25. package/skills/create-campaign-v2/references/lead-validation-preview.md +2 -2
  26. package/skills/create-campaign-v2/references/sample-validation-loop.md +32 -27
  27. package/skills/create-campaign-v2/references/step-13-import-leads.md +29 -28
  28. package/skills/create-campaign-v2/references/watch-guide-narration.md +27 -28
  29. package/skills/create-campaign-v2-tail/SKILL.md +44 -44
  30. package/skills/create-rubric/SKILL.md +5 -5
  31. package/skills/find-leads/SKILL.md +2 -2
  32. package/skills/generate-messages/SKILL.md +2 -1
  33. package/skills/providers/prospeo.md +3 -3
  34. package/skills/providers/sales-nav.md +7 -7
  35. package/skills/providers/signal-discovery.md +11 -11
@@ -29,29 +29,30 @@ const defaultSignalDiscoveryConfig = {
29
29
  };
30
30
  const defaultCampaignSourceDefaults = {
31
31
  reviewBatch: {
32
- defaultSize: 25,
33
- minProjectedPass: 5,
32
+ defaultSize: 15,
33
+ minProjectedPass: 1,
34
34
  },
35
35
  planning: {
36
36
  minFitRate: 0.1,
37
37
  },
38
38
  providers: {
39
39
  "signal-discovery": {
40
- targetGoodFitLeads: 150,
41
- defaultFitRate: 0.15,
40
+ targetGoodFitLeads: 300,
41
+ defaultFitRate: 0.2,
42
42
  maxSourceCandidates: 2500,
43
43
  postCoverageBuffer: 1.2,
44
44
  },
45
45
  "sales-nav": {
46
- targetGoodFitLeads: 150,
46
+ targetGoodFitLeads: 300,
47
47
  maxSourceCandidates: 2500,
48
48
  },
49
49
  prospeo: {
50
- targetGoodFitLeads: 150,
50
+ targetGoodFitLeads: 300,
51
51
  maxSourceCandidates: 2500,
52
52
  },
53
53
  },
54
54
  };
55
+ const defaultProviderSourceListTarget = 1000;
55
56
  const prospeoFilterValueSchema = {
56
57
  type: "object",
57
58
  description: "Include/exclude list filter (values must match Prospeo enums)",
@@ -517,7 +518,7 @@ function buildSourceImportWatchNarration({ provider, selectedPostCount, estimate
517
518
  : ""}`
518
519
  : `the approved ${providerLabel} source`;
519
520
  const targetDetail = typeof targetLeadCount === "number"
520
- ? ` Targeting ${targetLeadCount.toLocaleString("en-US")} source leads before the bounded review batch is cloned.`
521
+ ? ` Targeting ${targetLeadCount.toLocaleString("en-US")} source leads before the first review sample is processed.`
521
522
  : "";
522
523
  return {
523
524
  stage: "find-leads",
@@ -525,9 +526,9 @@ function buildSourceImportWatchNarration({ provider, selectedPostCount, estimate
525
526
  ? "Scraping source leads from posts"
526
527
  : "Importing source leads",
527
528
  visibleState: `The browser is showing ${providerLabel} import progress for ${sourceDetail}.${targetDetail}`,
528
- agentIntent: "Codex is materializing the approved source into a lead list before cloning only the bounded review batch into the campaign.",
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.",
529
+ 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.",
531
532
  progressLabel: "Source scouting",
532
533
  };
533
534
  }
@@ -551,8 +552,8 @@ function buildSourceImportRecoveryWatchNarration(args) {
551
552
  headline: args.reason === "failed" || args.reason === "zero"
552
553
  ? "Source import needs attention"
553
554
  : "Source import still running",
554
- visibleState: `${reasonCopy} The browser should stay on the lead import/review-batch screen.`,
555
- agentIntent: "Codex is holding the campaign before filter-choice until a non-empty bounded review batch exists.",
555
+ visibleState: `${reasonCopy} The browser should stay on the source-list import screen.`,
556
+ agentIntent: "Codex is holding the campaign before filter-choice until confirmed source rows exist in the campaign table.",
556
557
  nextAction: args.reason === "failed" || args.reason === "zero"
557
558
  ? "Retry the import or change the approved source"
558
559
  : "Wait again, retry readiness, or change the source",
@@ -666,8 +667,8 @@ function buildFilterChoiceWatchNarration({ sourceLeadCount, reviewRowCount, samp
666
667
  ? ` from ${sourceLeadCount.toLocaleString("en-US")} source candidate${sourceLeadCount === 1 ? "" : "s"}`
667
668
  : "";
668
669
  const reviewCopy = typeof reviewRowCount === "number" && reviewRowCount > 0
669
- ? `${reviewRowCount.toLocaleString("en-US")} review lead${reviewRowCount === 1 ? " is" : "s are"} in the campaign table${sourceCopy}.`
670
- : `The bounded review batch is in the campaign table${sourceCopy}.`;
670
+ ? `${reviewRowCount.toLocaleString("en-US")} review/process lead${reviewRowCount === 1 ? " is" : "s are"} in the campaign table${sourceCopy}.`
671
+ : `The review/process sample is in the campaign table${sourceCopy}.`;
671
672
  const assessment = analyzeFilterChoiceSample(sampleRows);
672
673
  const fitCount = assessment.strongFitCount + assessment.adjacentCount;
673
674
  const cleanupCount = assessment.riskCount + assessment.unknownCount;
@@ -734,7 +735,7 @@ function buildSelectedPostApprovalWatchNarration(selectedPostCount) {
734
735
  stage: "find-leads",
735
736
  headline: "Approve selected-post scrape",
736
737
  visibleState: `${selectedPostCount.toLocaleString("en-US")} LinkedIn post${selectedPostCount === 1 ? "" : "s"} selected in Signal Discovery.`,
737
- agentIntent: "Codex is asking before scraping this selected engager pool into a source list and cloning only the bounded review batch into the campaign.",
738
+ agentIntent: "Codex is asking before scraping this selected engager pool into a source list.",
738
739
  nextAction: `Approve scraping ${selectedPostCount.toLocaleString("en-US")} Signal Discovery post${selectedPostCount === 1 ? "" : "s"}`,
739
740
  safety: "Scrape approval is the next gate.",
740
741
  };
@@ -761,10 +762,11 @@ function buildSignalDiscoverySourceRecommendation({ selectedPosts, }) {
761
762
 
762
763
  Use Signal Discovery first.
763
764
 
764
- **Good-fit target:** ~${targetGoodFitLeads.toLocaleString("en-US")} prospects after cleanup, enrichment, and filters<br>
765
- **Source-candidate plan:** scrape ~${sourceCandidateTarget.toLocaleString("en-US")} raw engagers using a conservative ${Math.round(defaultFitRate * 100)}% fit-rate assumption<br>
765
+ **Goal:** ~${targetGoodFitLeads.toLocaleString("en-US")} good-fit prospects after cleanup, enrichment, and filters<br>
766
+ **Working assumption:** ~${Math.round(defaultFitRate * 100)}% of raw post engagers become good-fit prospects<br>
767
+ **Engagers needed:** ~${sourceCandidateTarget.toLocaleString("en-US")} raw engagers<br>
766
768
  **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>
767
- **Review checkpoint:** import the first ${reviewBatchSize.toLocaleString("en-US")} leads into the campaign for fit and message review before scaling
769
+ **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
768
770
 
769
771
  ### Selected posts
770
772
 
@@ -779,9 +781,9 @@ ${tableRows || "| Selected posts | Campaign-matched public engagement | - |"}
779
781
 
780
782
  Approve scraping these ${selectedCount} posts.
781
783
 
782
- This gives enough volume to work toward ~${targetGoodFitLeads.toLocaleString("en-US")} good-fit prospects while keeping the source tied to people already engaging with the campaign's strongest public buying signals.
784
+ 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.
783
785
 
784
- **First pass:** build the source list, then import only the ${reviewBatchSize.toLocaleString("en-US")}-lead review batch so we can inspect fit and messages before scaling.
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.
785
787
 
786
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.
787
789
 
@@ -1324,7 +1326,7 @@ export const leadToolDefinitions = [
1324
1326
  },
1325
1327
  {
1326
1328
  name: "import_leads",
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.",
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.",
1328
1330
  inputSchema: {
1329
1331
  type: "object",
1330
1332
  properties: {
@@ -1351,7 +1353,7 @@ export const leadToolDefinitions = [
1351
1353
  },
1352
1354
  targetLeadCount: {
1353
1355
  type: "number",
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.",
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.",
1355
1357
  },
1356
1358
  mode: {
1357
1359
  type: "string",
@@ -1373,7 +1375,7 @@ export const leadToolDefinitions = [
1373
1375
  },
1374
1376
  targetEngagerCount: {
1375
1377
  type: "number",
1376
- description: "Signal Discovery: target number of post engagers/source candidates to scrape. Compute from source target good-fit leads / sampled fit rate; e.g. 150 good fits at 15% fit requires about 1000 engagers. 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.",
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.",
1377
1379
  },
1378
1380
  maxPostsToScrape: {
1379
1381
  type: "number",
@@ -1417,7 +1419,7 @@ export const leadToolDefinitions = [
1417
1419
  },
1418
1420
  {
1419
1421
  name: "confirm_lead_list",
1420
- description: "After the user confirms the lead list looks good, import the bounded review batch into the campaign table. This tool owns moving the watched campaign to filter-choice with sample-assessment narration only after non-empty review rows exist; do not call update_campaign to fix filter-choice afterward. selectedLeadListId remains the source lead list; workflowTableId is the campaign table.",
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.",
1421
1423
  inputSchema: {
1422
1424
  type: "object",
1423
1425
  properties: {
@@ -1447,7 +1449,11 @@ export const leadToolDefinitions = [
1447
1449
  },
1448
1450
  targetLeadCount: {
1449
1451
  type: "number",
1450
- description: "Target lead count requested. Also caps the campaign-table clone for bounded review batches.",
1452
+ description: "Deprecated alias for reviewBatchLimit. Does not cap the campaign-table clone; confirmed source rows are copied into the campaign table.",
1453
+ },
1454
+ reviewBatchLimit: {
1455
+ type: "number",
1456
+ description: "Number of campaign rows to use as the initial review/process sample. Defaults to 15.",
1451
1457
  },
1452
1458
  confirmed: {
1453
1459
  type: "boolean",
@@ -2399,12 +2405,17 @@ export async function importLeads(input) {
2399
2405
  const maxImportCount = getMaxImportCount(provider);
2400
2406
  const normalizedTargetLeadCount = normalizeTargetLeadCount(targetLeadCount, maxImportCount);
2401
2407
  const requestedLeadCount = normalizedTargetLeadCount ??
2402
- (provider === "sales-nav" || provider === "prospeo" ? 100 : undefined);
2408
+ (provider === "sales-nav" || provider === "prospeo"
2409
+ ? defaultProviderSourceListTarget
2410
+ : undefined);
2403
2411
  const cappedTargetLeadCount = requestedLeadCount !== undefined
2404
2412
  ? Math.min(requestedLeadCount, maxImportCount)
2405
2413
  : undefined;
2406
2414
  // === SIGNAL DISCOVERY FLOW ===
2407
2415
  if (provider === "signal-discovery") {
2416
+ const defaultSignalTargetEngagers = getSignalDiscoverySourcePlanDefaults().sourceCandidateTarget;
2417
+ const effectiveTargetEngagerCount = normalizePositiveInteger(targetEngagerCount) ??
2418
+ defaultSignalTargetEngagers;
2408
2419
  // Get selected posts from the campaign's signal search tabs
2409
2420
  // Note: API returns flat fields (postUrl, postContent, authorName, etc.)
2410
2421
  const tabsResponse = await api.get(`/api/v3/campaigns/${campaignOfferId}/signal-discovery/tabs`);
@@ -2449,7 +2460,7 @@ export async function importLeads(input) {
2449
2460
  }
2450
2461
  const uniqueSelectedPosts = Array.from(uniqueByUrl.values());
2451
2462
  const importSelection = selectSignalPostsForImport(uniqueSelectedPosts, {
2452
- targetEngagerCount,
2463
+ targetEngagerCount: effectiveTargetEngagerCount,
2453
2464
  maxPostsToScrape,
2454
2465
  });
2455
2466
  const postsToScrape = importSelection.posts;
@@ -2485,8 +2496,7 @@ export async function importLeads(input) {
2485
2496
  provider: "signal-discovery",
2486
2497
  selectedPostCount: postsToScrape.length,
2487
2498
  estimatedEngagers: result.estimatedEngagers,
2488
- targetLeadCount: normalizePositiveInteger(targetEngagerCount) ??
2489
- result.estimatedEngagers,
2499
+ targetLeadCount: effectiveTargetEngagerCount,
2490
2500
  }),
2491
2501
  }
2492
2502
  : {}),
@@ -2498,7 +2508,7 @@ export async function importLeads(input) {
2498
2508
  estimatedEngagers: result.estimatedEngagers,
2499
2509
  selectedPostCount: postsToScrape.length,
2500
2510
  availableSelectedPostCount: uniqueSelectedPosts.length,
2501
- targetEngagerCount: normalizePositiveInteger(targetEngagerCount) ?? null,
2511
+ targetEngagerCount: effectiveTargetEngagerCount,
2502
2512
  maxPostsToScrape: normalizePositiveInteger(maxPostsToScrape) ?? null,
2503
2513
  limitedSelectedPosts: importSelection.limited,
2504
2514
  targetLeadCount: cappedTargetLeadCount ?? null,
@@ -2540,7 +2550,7 @@ export async function importLeads(input) {
2540
2550
  searchId,
2541
2551
  workflowTableId: leadListId,
2542
2552
  campaignOfferId,
2543
- targetLeadCount: cappedTargetLeadCount ?? 100,
2553
+ targetLeadCount: cappedTargetLeadCount ?? defaultProviderSourceListTarget,
2544
2554
  ...(normalizedMode ? { mode: normalizedMode } : {}),
2545
2555
  });
2546
2556
  }
@@ -2548,7 +2558,7 @@ export async function importLeads(input) {
2548
2558
  return api.post(`/api/v3/lead-lists/${leadListId}/prospeo-import/start`, {
2549
2559
  searchId,
2550
2560
  campaignOfferId,
2551
- targetLeadCount: cappedTargetLeadCount,
2561
+ targetLeadCount: cappedTargetLeadCount ?? defaultProviderSourceListTarget,
2552
2562
  ...(normalizedMode ? { mode: normalizedMode } : {}),
2553
2563
  });
2554
2564
  }
@@ -2664,7 +2674,7 @@ export async function cancelLeadImport(input) {
2664
2674
  }
2665
2675
  export async function confirmLeadList(input) {
2666
2676
  const api = getApi();
2667
- const { campaignOfferId, currentStep, confirmed, sourceLeadListId, campaignName, keepInSync, jobId, targetLeadCount, } = input;
2677
+ const { campaignOfferId, currentStep, confirmed, sourceLeadListId, campaignName, keepInSync, jobId, reviewBatchLimit, targetLeadCount, } = input;
2668
2678
  assertInteractionApproval({
2669
2679
  campaignId: campaignOfferId,
2670
2680
  action: "confirm-lead-list",
@@ -2736,7 +2746,7 @@ export async function confirmLeadList(input) {
2736
2746
  leadListConfig?.importStatus === "complete" &&
2737
2747
  leadListRowCount > 0 &&
2738
2748
  leadListRowCount < signalSourceTargetLeadCount) {
2739
- throw new Error(`Signal Discovery source list is under capacity: it completed with ${leadListRowCount.toLocaleString("en-US")} source candidates, below the approved ${signalSourceTargetLeadCount.toLocaleString("en-US")} source-candidate target. Do not import the bounded review batch from this source. Select more posts, rerun Signal Discovery, or move to Sales Nav/Prospeo.`);
2749
+ throw new Error(`Signal Discovery source list is under capacity: it completed with ${leadListRowCount.toLocaleString("en-US")} source candidates, below the approved ${signalSourceTargetLeadCount.toLocaleString("en-US")} source-candidate target. Do not confirm this source list. Select more posts, rerun Signal Discovery, or move to Sales Nav/Prospeo.`);
2740
2750
  }
2741
2751
  const progressProcessed = typeof importProgress?.processed === "number"
2742
2752
  ? importProgress.processed
@@ -2770,12 +2780,17 @@ export async function confirmLeadList(input) {
2770
2780
  };
2771
2781
  }
2772
2782
  else {
2783
+ const readinessTargetLeadCount = typeof leadListConfig?.targetLeadCount === "number" &&
2784
+ Number.isFinite(leadListConfig.targetLeadCount) &&
2785
+ leadListConfig.targetLeadCount > 0
2786
+ ? leadListConfig.targetLeadCount
2787
+ : undefined;
2773
2788
  readiness = await waitForLeadListReady({
2774
2789
  leadListId: resolvedLeadListId,
2775
2790
  campaignOfferId,
2776
2791
  provider: resolvedProvider,
2777
2792
  jobId,
2778
- targetLeadCount,
2793
+ targetLeadCount: readinessTargetLeadCount,
2779
2794
  timeoutMs: 5000,
2780
2795
  intervalMs: 1000,
2781
2796
  });
@@ -2814,7 +2829,6 @@ export async function confirmLeadList(input) {
2814
2829
  campaignOfferId,
2815
2830
  campaignName,
2816
2831
  keepInSync,
2817
- ...(typeof targetLeadCount === "number" ? { targetLeadCount } : {}),
2818
2832
  currentStep: null,
2819
2833
  })
2820
2834
  .catch((error) => {
@@ -2824,15 +2838,13 @@ export async function confirmLeadList(input) {
2824
2838
  throw error;
2825
2839
  });
2826
2840
  const campaignTableId = importResult.workflowTableId ?? importResult.campaignTableId;
2827
- const requestedTargetLeadCount = typeof targetLeadCount === "number" && Number.isFinite(targetLeadCount)
2828
- ? Math.max(1, Math.floor(targetLeadCount))
2829
- : null;
2841
+ const defaults = loadCampaignSourceDefaults();
2842
+ const requestedReviewBatchLimit = normalizePositiveInteger(reviewBatchLimit) ??
2843
+ normalizePositiveInteger(targetLeadCount) ??
2844
+ defaults.reviewBatch.defaultSize;
2830
2845
  const importedRowIds = Array.isArray(importResult.rowIds)
2831
2846
  ? importResult.rowIds.filter((rowId) => typeof rowId === "string" && rowId.trim().length > 0)
2832
2847
  : [];
2833
- const overflowRowIds = campaignTableId && requestedTargetLeadCount !== null
2834
- ? importedRowIds.slice(requestedTargetLeadCount)
2835
- : [];
2836
2848
  const importedRowCount = importedRowIds.length > 0
2837
2849
  ? importedRowIds.length
2838
2850
  : typeof importResult.rowCount === "number"
@@ -2843,21 +2855,22 @@ export async function confirmLeadList(input) {
2843
2855
  : typeof importResult.leadsImported === "number"
2844
2856
  ? importResult.leadsImported
2845
2857
  : 0;
2846
- const keptReviewRowCount = requestedTargetLeadCount !== null
2847
- ? Math.min(importedRowCount, requestedTargetLeadCount)
2848
- : importedRowCount;
2858
+ const keptReviewRowCount = Math.min(importedRowCount, requestedReviewBatchLimit);
2849
2859
  const remainingRowCount = typeof importResult.remainingRowCount === "number"
2850
2860
  ? importResult.remainingRowCount
2851
2861
  : 0;
2852
2862
  const importStatus = String(importResult.importStatus ?? "").toLowerCase();
2863
+ const copyStillRunning = importResult.async === true &&
2864
+ (importResult.hybrid === true || remainingRowCount > 0);
2853
2865
  const campaignTableReady = importResult.campaignTableReady === true ||
2866
+ (importResult.hybrid === true && importedRowCount > 0) ||
2854
2867
  (!importResult.async &&
2855
2868
  importStatus !== "pending" &&
2856
2869
  importStatus !== "partial" &&
2857
2870
  remainingRowCount <= 0 &&
2858
2871
  importedRowCount > 0);
2859
2872
  if (!campaignTableReady) {
2860
- throw new Error("Campaign review rows are still importing. Stay on lead import until the bounded campaign table is ready, then retry confirm_lead_list or wait_for_campaign_table_ready.");
2873
+ 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.");
2861
2874
  }
2862
2875
  if (importedRowCount <= 0) {
2863
2876
  const recoveryNarration = buildSourceImportRecoveryWatchNarration({
@@ -2866,17 +2879,10 @@ export async function confirmLeadList(input) {
2866
2879
  });
2867
2880
  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}`);
2868
2881
  }
2869
- if (campaignTableId && overflowRowIds.length > 0) {
2870
- const deleteBatchSize = 25;
2871
- for (let index = 0; index < overflowRowIds.length; index += deleteBatchSize) {
2872
- const batch = overflowRowIds.slice(index, index + deleteBatchSize);
2873
- await Promise.all(batch.map((rowId) => api.delete(`/api/v3/workflow-tables/${campaignTableId}/rows/${rowId}`)));
2874
- }
2875
- }
2876
2882
  let reviewSampleRows = [];
2877
2883
  if (campaignTableId && effectiveCurrentStep === "filter-choice") {
2878
2884
  try {
2879
- const sampleLimit = Math.min(Math.max(keptReviewRowCount, 1), 25);
2885
+ const sampleLimit = Math.min(Math.max(keptReviewRowCount, 1), requestedReviewBatchLimit);
2880
2886
  const sample = await getTableRowsMinimal(campaignTableId, {
2881
2887
  limit: sampleLimit,
2882
2888
  page: 1,
@@ -2924,31 +2930,37 @@ export async function confirmLeadList(input) {
2924
2930
  workflowTableId: campaignTableId ?? null,
2925
2931
  reviewBatchRowIds: importedRowIds.slice(0, keptReviewRowCount),
2926
2932
  reviewBatchRowCount: keptReviewRowCount,
2933
+ copiedCampaignRowCount: importedRowCount,
2927
2934
  },
2928
2935
  branchBasisFields: [
2929
2936
  "campaign revision or updatedAt",
2930
2937
  "brief hash",
2931
2938
  "selectedLeadListId",
2932
2939
  "workflowTableId",
2933
- "bounded review-batch row ids/hash",
2940
+ "first review/process sample row ids/hash",
2934
2941
  "filter choice at branch start",
2935
2942
  ],
2936
2943
  promptRequired: 'Load get_subskill_prompt({ subskillName: "generate-messages", offset, limit }) until hasMore=false before drafting.',
2937
2944
  },
2938
- boundedReviewBatch: requestedTargetLeadCount !== null
2939
- ? {
2940
- requestedTargetLeadCount,
2941
- importedRowCount: importedRowIds.length,
2942
- keptRowCount: importedRowIds.length > 0
2943
- ? Math.min(importedRowIds.length, requestedTargetLeadCount)
2944
- : null,
2945
- trimmedOverflowRowCount: overflowRowIds.length,
2946
- }
2947
- : undefined,
2948
- message: requestedTargetLeadCount !== null &&
2949
- leadListRowCount > requestedTargetLeadCount
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.",
2945
+ boundedReviewBatch: {
2946
+ requestedTargetLeadCount: requestedReviewBatchLimit,
2947
+ requestedReviewBatchLimit,
2948
+ importedRowCount,
2949
+ keptRowCount: keptReviewRowCount,
2950
+ reviewRowCount: keptReviewRowCount,
2951
+ copiedCampaignRowCount: importedRowCount,
2952
+ sourceCandidateRowCount: importResult.sourceRowCount ??
2953
+ importResult.clonedSourceRowCount ??
2954
+ leadListRowCount,
2955
+ remainingCopyRowCount: remainingRowCount,
2956
+ copyStillRunning,
2957
+ trimmedOverflowRowCount: 0,
2958
+ },
2959
+ message: copyStillRunning
2960
+ ? `First ${importedRowCount.toLocaleString("en-US")} source candidate${importedRowCount === 1 ? "" : "s"} are copied into the campaign table and the rest of the confirmed source list is still copying. Use the first ${keptReviewRowCount.toLocaleString("en-US")} as the review/process sample. The watched campaign is now on filter-choice; ask add filters vs skip filters before loading post-lead workers.`
2961
+ : importedRowCount > keptReviewRowCount
2962
+ ? `Copied ${importedRowCount.toLocaleString("en-US")} source candidate${importedRowCount === 1 ? "" : "s"} into the campaign table. Use the first ${keptReviewRowCount.toLocaleString("en-US")} as the review/process sample. The watched campaign is now on filter-choice; ask add filters vs skip filters before loading post-lead workers.`
2963
+ : `Copied ${importedRowCount.toLocaleString("en-US")} source candidate${importedRowCount === 1 ? "" : "s"} into the campaign table for the review/process sample. The watched campaign is now on filter-choice; ask add filters vs skip filters before loading post-lead workers.`,
2952
2964
  };
2953
2965
  }
2954
2966
  export function getProviderPrompt(input) {
@@ -3058,7 +3070,7 @@ Use Signal Discovery first.
3058
3070
 
3059
3071
  **Recommendation:** approve scraping the ${selectionResult.selectedCount} selected Signal Discovery post${selectionResult.selectedCount === 1 ? "" : "s"}.
3060
3072
 
3061
- **First pass:** build the source list, then import only the ${reviewBatchSize.toLocaleString("en-US")}-lead review batch so we can inspect fit and messages before scaling.
3073
+ **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.
3062
3074
 
3063
3075
  Approval card should say:
3064
3076
 
@@ -3071,7 +3083,7 @@ Approval card should say:
3071
3083
  criteriaCount: selectionResult.criteriaCount,
3072
3084
  message: `${sourceRecommendation}
3073
3085
 
3074
- Selected ${selectionResult.selectedCount} posts with ${selectionResult.criteriaCount} ICP criteria persisted. Ask the user to approve the specific scraping action before calling import_leads; the earlier source-plan approval only authorized search/scouting.`,
3086
+ Selected ${selectionResult.selectedCount} posts with ${selectionResult.criteriaCount} ICP criteria persisted. Ask the user to approve this specific scraping action once; after approval, call import_leads immediately and do not repeat this source card.`,
3075
3087
  };
3076
3088
  }
3077
3089
  export async function setHeadlineICPCriteria(input) {
@@ -260,7 +260,7 @@ export function getPostFindLeadsScoutRegistry() {
260
260
  "campaignBrief",
261
261
  "source decision and selectedLeadList/source state",
262
262
  "workflowTableId",
263
- "imported review-batch rows from selectedLeadList with row ids/hash",
263
+ "first review/process sample 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 imports a non-empty bounded review batch and get_rows_minimal confirms rows for workflowTableId",
300
+ firstAllowedStart: "after confirm_lead_list copies source rows and the first review/process sample exists",
301
301
  forbiddenStarts: [
302
302
  "source recommendation",
303
303
  "provider import job alone",
304
- "zero-row review batch",
304
+ "zero-row review/process sample",
305
305
  ],
306
306
  runtimeProofTransport: "CampaignOffer.watchNarration.workerDetails.messageDraftBuilder",
307
307
  runtimeProofRequiredFields: [
@@ -313,7 +313,7 @@ export function getPostFindLeadsScoutRegistry() {
313
313
  "basisToken",
314
314
  "basis.selectedLeadListId",
315
315
  "basis.workflowTableId",
316
- "basis.reviewBatchRowHash or basis.reviewBatchRowIds",
316
+ "basis.reviewSampleRowHash or basis.reviewSampleRowIds",
317
317
  ],
318
318
  promptRequired: 'get_subskill_prompt({ subskillName: "generate-messages", offset, limit }) until hasMore=false',
319
319
  basisFields: [
@@ -321,7 +321,7 @@ export function getPostFindLeadsScoutRegistry() {
321
321
  "brief hash",
322
322
  "selectedLeadListId",
323
323
  "workflowTableId",
324
- "bounded review-batch row ids/hash",
324
+ "first review/process sample row ids/hash",
325
325
  "filter choice",
326
326
  "filter/rubric basis when present",
327
327
  ],
@@ -337,12 +337,12 @@ export function getPostFindLeadsScoutRegistry() {
337
337
  "outputHash",
338
338
  "error or retry detail",
339
339
  ],
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-batch identity mismatches or the initial output failed.",
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 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.",
344
- 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.",
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 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 get_subskill_prompt({ subskillName: "generate-messages", offset, limit }) until hasMore=false, 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. 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 batch. 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 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.',
346
346
  },
347
347
  };
348
348
  }
@@ -1391,6 +1391,7 @@ export declare const allTools: ({
1391
1391
  campaignName?: undefined;
1392
1392
  keepInSync?: undefined;
1393
1393
  jobId?: undefined;
1394
+ reviewBatchLimit?: undefined;
1394
1395
  selections?: undefined;
1395
1396
  selectionMode?: undefined;
1396
1397
  };
@@ -1566,6 +1567,7 @@ export declare const allTools: ({
1566
1567
  campaignName?: undefined;
1567
1568
  keepInSync?: undefined;
1568
1569
  jobId?: undefined;
1570
+ reviewBatchLimit?: undefined;
1569
1571
  selections?: undefined;
1570
1572
  selectionMode?: undefined;
1571
1573
  };
@@ -1640,6 +1642,7 @@ export declare const allTools: ({
1640
1642
  campaignName?: undefined;
1641
1643
  keepInSync?: undefined;
1642
1644
  jobId?: undefined;
1645
+ reviewBatchLimit?: undefined;
1643
1646
  selections?: undefined;
1644
1647
  selectionMode?: undefined;
1645
1648
  };
@@ -1786,6 +1789,7 @@ export declare const allTools: ({
1786
1789
  campaignName?: undefined;
1787
1790
  keepInSync?: undefined;
1788
1791
  jobId?: undefined;
1792
+ reviewBatchLimit?: undefined;
1789
1793
  selections?: undefined;
1790
1794
  selectionMode?: undefined;
1791
1795
  };
@@ -1874,6 +1878,7 @@ export declare const allTools: ({
1874
1878
  campaignName?: undefined;
1875
1879
  keepInSync?: undefined;
1876
1880
  jobId?: undefined;
1881
+ reviewBatchLimit?: undefined;
1877
1882
  selections?: undefined;
1878
1883
  selectionMode?: undefined;
1879
1884
  };
@@ -1971,6 +1976,7 @@ export declare const allTools: ({
1971
1976
  campaignName?: undefined;
1972
1977
  keepInSync?: undefined;
1973
1978
  jobId?: undefined;
1979
+ reviewBatchLimit?: undefined;
1974
1980
  selections?: undefined;
1975
1981
  selectionMode?: undefined;
1976
1982
  };
@@ -2050,6 +2056,7 @@ export declare const allTools: ({
2050
2056
  campaignName?: undefined;
2051
2057
  keepInSync?: undefined;
2052
2058
  jobId?: undefined;
2059
+ reviewBatchLimit?: undefined;
2053
2060
  selections?: undefined;
2054
2061
  selectionMode?: undefined;
2055
2062
  };
@@ -2668,6 +2675,7 @@ export declare const allTools: ({
2668
2675
  campaignName?: undefined;
2669
2676
  keepInSync?: undefined;
2670
2677
  jobId?: undefined;
2678
+ reviewBatchLimit?: undefined;
2671
2679
  selections?: undefined;
2672
2680
  selectionMode?: undefined;
2673
2681
  };
@@ -2799,6 +2807,7 @@ export declare const allTools: ({
2799
2807
  campaignName?: undefined;
2800
2808
  keepInSync?: undefined;
2801
2809
  jobId?: undefined;
2810
+ reviewBatchLimit?: undefined;
2802
2811
  selections?: undefined;
2803
2812
  selectionMode?: undefined;
2804
2813
  };
@@ -2918,6 +2927,7 @@ export declare const allTools: ({
2918
2927
  campaignName?: undefined;
2919
2928
  keepInSync?: undefined;
2920
2929
  jobId?: undefined;
2930
+ reviewBatchLimit?: undefined;
2921
2931
  selections?: undefined;
2922
2932
  selectionMode?: undefined;
2923
2933
  };
@@ -2995,6 +3005,7 @@ export declare const allTools: ({
2995
3005
  campaignName?: undefined;
2996
3006
  keepInSync?: undefined;
2997
3007
  jobId?: undefined;
3008
+ reviewBatchLimit?: undefined;
2998
3009
  selections?: undefined;
2999
3010
  selectionMode?: undefined;
3000
3011
  };
@@ -3034,6 +3045,10 @@ export declare const allTools: ({
3034
3045
  type: string;
3035
3046
  description: string;
3036
3047
  };
3048
+ reviewBatchLimit: {
3049
+ type: string;
3050
+ description: string;
3051
+ };
3037
3052
  confirmed: {
3038
3053
  type: string;
3039
3054
  description: string;
@@ -3192,6 +3207,7 @@ export declare const allTools: ({
3192
3207
  campaignName?: undefined;
3193
3208
  keepInSync?: undefined;
3194
3209
  jobId?: undefined;
3210
+ reviewBatchLimit?: undefined;
3195
3211
  };
3196
3212
  required: string[];
3197
3213
  };
@@ -3271,6 +3287,7 @@ export declare const allTools: ({
3271
3287
  campaignName?: undefined;
3272
3288
  keepInSync?: undefined;
3273
3289
  jobId?: undefined;
3290
+ reviewBatchLimit?: undefined;
3274
3291
  selections?: undefined;
3275
3292
  selectionMode?: undefined;
3276
3293
  };