@sellable/mcp 0.1.151 → 0.1.153

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 (37) 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 +37 -3
  11. package/dist/tools/leads.js +85 -77
  12. package/dist/tools/prompts.js +9 -9
  13. package/dist/tools/readiness.d.ts +7 -1
  14. package/dist/tools/readiness.js +10 -17
  15. package/dist/tools/registry.d.ts +17 -0
  16. package/dist/tools/rubrics.js +23 -20
  17. package/package.json +1 -1
  18. package/skills/create-campaign/SKILL.md +59 -56
  19. package/skills/create-campaign-v2/SKILL.md +44 -42
  20. package/skills/create-campaign-v2/SOUL.md +16 -13
  21. package/skills/create-campaign-v2/core/auto-execute.README.md +16 -17
  22. package/skills/create-campaign-v2/core/auto-execute.yaml +8 -7
  23. package/skills/create-campaign-v2/core/flow.v2.json +81 -149
  24. package/skills/create-campaign-v2/core/policy.md +13 -12
  25. package/skills/create-campaign-v2/references/approval-gate-framing.md +4 -3
  26. package/skills/create-campaign-v2/references/filter-leads.md +5 -4
  27. package/skills/create-campaign-v2/references/lead-validation-preview.md +2 -2
  28. package/skills/create-campaign-v2/references/sample-validation-loop.md +32 -27
  29. package/skills/create-campaign-v2/references/step-13-import-leads.md +44 -33
  30. package/skills/create-campaign-v2/references/watch-guide-narration.md +27 -28
  31. package/skills/create-campaign-v2-tail/SKILL.md +44 -44
  32. package/skills/create-rubric/SKILL.md +5 -5
  33. package/skills/find-leads/SKILL.md +2 -2
  34. package/skills/generate-messages/SKILL.md +2 -1
  35. package/skills/providers/prospeo.md +3 -3
  36. package/skills/providers/sales-nav.md +7 -7
  37. package/skills/providers/signal-discovery.md +47 -23
@@ -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",
@@ -2731,13 +2741,6 @@ export async function confirmLeadList(input) {
2731
2741
  leadListConfig.targetLeadCount > 0
2732
2742
  ? leadListConfig.targetLeadCount
2733
2743
  : null;
2734
- if (resolvedProvider === "signal-discovery" &&
2735
- signalSourceTargetLeadCount !== null &&
2736
- leadListConfig?.importStatus === "complete" &&
2737
- leadListRowCount > 0 &&
2738
- 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.`);
2740
- }
2741
2744
  const progressProcessed = typeof importProgress?.processed === "number"
2742
2745
  ? importProgress.processed
2743
2746
  : null;
@@ -2770,12 +2773,17 @@ export async function confirmLeadList(input) {
2770
2773
  };
2771
2774
  }
2772
2775
  else {
2776
+ const readinessTargetLeadCount = typeof leadListConfig?.targetLeadCount === "number" &&
2777
+ Number.isFinite(leadListConfig.targetLeadCount) &&
2778
+ leadListConfig.targetLeadCount > 0
2779
+ ? leadListConfig.targetLeadCount
2780
+ : undefined;
2773
2781
  readiness = await waitForLeadListReady({
2774
2782
  leadListId: resolvedLeadListId,
2775
2783
  campaignOfferId,
2776
2784
  provider: resolvedProvider,
2777
2785
  jobId,
2778
- targetLeadCount,
2786
+ targetLeadCount: readinessTargetLeadCount,
2779
2787
  timeoutMs: 5000,
2780
2788
  intervalMs: 1000,
2781
2789
  });
@@ -2814,7 +2822,6 @@ export async function confirmLeadList(input) {
2814
2822
  campaignOfferId,
2815
2823
  campaignName,
2816
2824
  keepInSync,
2817
- ...(typeof targetLeadCount === "number" ? { targetLeadCount } : {}),
2818
2825
  currentStep: null,
2819
2826
  })
2820
2827
  .catch((error) => {
@@ -2824,15 +2831,13 @@ export async function confirmLeadList(input) {
2824
2831
  throw error;
2825
2832
  });
2826
2833
  const campaignTableId = importResult.workflowTableId ?? importResult.campaignTableId;
2827
- const requestedTargetLeadCount = typeof targetLeadCount === "number" && Number.isFinite(targetLeadCount)
2828
- ? Math.max(1, Math.floor(targetLeadCount))
2829
- : null;
2834
+ const defaults = loadCampaignSourceDefaults();
2835
+ const requestedReviewBatchLimit = normalizePositiveInteger(reviewBatchLimit) ??
2836
+ normalizePositiveInteger(targetLeadCount) ??
2837
+ defaults.reviewBatch.defaultSize;
2830
2838
  const importedRowIds = Array.isArray(importResult.rowIds)
2831
2839
  ? importResult.rowIds.filter((rowId) => typeof rowId === "string" && rowId.trim().length > 0)
2832
2840
  : [];
2833
- const overflowRowIds = campaignTableId && requestedTargetLeadCount !== null
2834
- ? importedRowIds.slice(requestedTargetLeadCount)
2835
- : [];
2836
2841
  const importedRowCount = importedRowIds.length > 0
2837
2842
  ? importedRowIds.length
2838
2843
  : typeof importResult.rowCount === "number"
@@ -2843,21 +2848,22 @@ export async function confirmLeadList(input) {
2843
2848
  : typeof importResult.leadsImported === "number"
2844
2849
  ? importResult.leadsImported
2845
2850
  : 0;
2846
- const keptReviewRowCount = requestedTargetLeadCount !== null
2847
- ? Math.min(importedRowCount, requestedTargetLeadCount)
2848
- : importedRowCount;
2851
+ const keptReviewRowCount = Math.min(importedRowCount, requestedReviewBatchLimit);
2849
2852
  const remainingRowCount = typeof importResult.remainingRowCount === "number"
2850
2853
  ? importResult.remainingRowCount
2851
2854
  : 0;
2852
2855
  const importStatus = String(importResult.importStatus ?? "").toLowerCase();
2856
+ const copyStillRunning = importResult.async === true &&
2857
+ (importResult.hybrid === true || remainingRowCount > 0);
2853
2858
  const campaignTableReady = importResult.campaignTableReady === true ||
2859
+ (importResult.hybrid === true && importedRowCount > 0) ||
2854
2860
  (!importResult.async &&
2855
2861
  importStatus !== "pending" &&
2856
2862
  importStatus !== "partial" &&
2857
2863
  remainingRowCount <= 0 &&
2858
2864
  importedRowCount > 0);
2859
2865
  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.");
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.");
2861
2867
  }
2862
2868
  if (importedRowCount <= 0) {
2863
2869
  const recoveryNarration = buildSourceImportRecoveryWatchNarration({
@@ -2866,17 +2872,10 @@ export async function confirmLeadList(input) {
2866
2872
  });
2867
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}`);
2868
2874
  }
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
2875
  let reviewSampleRows = [];
2877
2876
  if (campaignTableId && effectiveCurrentStep === "filter-choice") {
2878
2877
  try {
2879
- const sampleLimit = Math.min(Math.max(keptReviewRowCount, 1), 25);
2878
+ const sampleLimit = Math.min(Math.max(keptReviewRowCount, 1), requestedReviewBatchLimit);
2880
2879
  const sample = await getTableRowsMinimal(campaignTableId, {
2881
2880
  limit: sampleLimit,
2882
2881
  page: 1,
@@ -2924,31 +2923,40 @@ export async function confirmLeadList(input) {
2924
2923
  workflowTableId: campaignTableId ?? null,
2925
2924
  reviewBatchRowIds: importedRowIds.slice(0, keptReviewRowCount),
2926
2925
  reviewBatchRowCount: keptReviewRowCount,
2926
+ copiedCampaignRowCount: importedRowCount,
2927
2927
  },
2928
2928
  branchBasisFields: [
2929
2929
  "campaign revision or updatedAt",
2930
2930
  "brief hash",
2931
2931
  "selectedLeadListId",
2932
2932
  "workflowTableId",
2933
- "bounded review-batch row ids/hash",
2933
+ "first review/process sample row ids/hash",
2934
2934
  "filter choice at branch start",
2935
2935
  ],
2936
2936
  promptRequired: 'Load get_subskill_prompt({ subskillName: "generate-messages", offset, limit }) until hasMore=false before drafting.',
2937
2937
  },
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.",
2938
+ boundedReviewBatch: {
2939
+ requestedTargetLeadCount: requestedReviewBatchLimit,
2940
+ requestedReviewBatchLimit,
2941
+ importedRowCount,
2942
+ keptRowCount: keptReviewRowCount,
2943
+ reviewRowCount: keptReviewRowCount,
2944
+ copiedCampaignRowCount: importedRowCount,
2945
+ sourceCandidateRowCount: importResult.sourceRowCount ??
2946
+ importResult.clonedSourceRowCount ??
2947
+ leadListRowCount,
2948
+ remainingCopyRowCount: remainingRowCount,
2949
+ copyStillRunning,
2950
+ trimmedOverflowRowCount: 0,
2951
+ sourceShortfall: signalSourceTargetLeadCount !== null &&
2952
+ leadListRowCount > 0 &&
2953
+ leadListRowCount < signalSourceTargetLeadCount,
2954
+ },
2955
+ message: copyStillRunning
2956
+ ? `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.`
2957
+ : importedRowCount > keptReviewRowCount
2958
+ ? `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.`
2959
+ : `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
2960
  };
2953
2961
  }
2954
2962
  export function getProviderPrompt(input) {
@@ -3058,7 +3066,7 @@ Use Signal Discovery first.
3058
3066
 
3059
3067
  **Recommendation:** approve scraping the ${selectionResult.selectedCount} selected Signal Discovery post${selectionResult.selectedCount === 1 ? "" : "s"}.
3060
3068
 
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.
3069
+ **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
3070
 
3063
3071
  Approval card should say:
3064
3072
 
@@ -3071,7 +3079,7 @@ Approval card should say:
3071
3079
  criteriaCount: selectionResult.criteriaCount,
3072
3080
  message: `${sourceRecommendation}
3073
3081
 
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.`,
3082
+ 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
3083
  };
3076
3084
  }
3077
3085
  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
  }
@@ -273,6 +273,7 @@ export declare function waitForLeadListReady(input: WaitForLeadListReadyInput):
273
273
  status?: undefined;
274
274
  targetLeadCount?: undefined;
275
275
  error?: undefined;
276
+ sourceShortfall?: undefined;
276
277
  warning?: undefined;
277
278
  } | {
278
279
  ready: boolean;
@@ -285,6 +286,7 @@ export declare function waitForLeadListReady(input: WaitForLeadListReadyInput):
285
286
  status: string;
286
287
  targetLeadCount: number | undefined;
287
288
  error?: undefined;
289
+ sourceShortfall?: undefined;
288
290
  warning?: undefined;
289
291
  } | {
290
292
  ready: boolean;
@@ -297,6 +299,7 @@ export declare function waitForLeadListReady(input: WaitForLeadListReadyInput):
297
299
  status: string | null;
298
300
  targetLeadCount: number | undefined;
299
301
  error: string;
302
+ sourceShortfall?: undefined;
300
303
  warning?: undefined;
301
304
  } | {
302
305
  ready: boolean;
@@ -309,6 +312,7 @@ export declare function waitForLeadListReady(input: WaitForLeadListReadyInput):
309
312
  status: string;
310
313
  targetLeadCount: number | undefined;
311
314
  error: string | null;
315
+ sourceShortfall?: undefined;
312
316
  warning?: undefined;
313
317
  } | {
314
318
  ready: boolean;
@@ -319,9 +323,10 @@ export declare function waitForLeadListReady(input: WaitForLeadListReadyInput):
319
323
  rowCount: number;
320
324
  status: string | null;
321
325
  targetLeadCount: number | null;
326
+ sourceShortfall: boolean;
327
+ warning: string | undefined;
322
328
  reason?: undefined;
323
329
  error?: undefined;
324
- warning?: undefined;
325
330
  } | {
326
331
  ready: boolean;
327
332
  reason: string;
@@ -334,5 +339,6 @@ export declare function waitForLeadListReady(input: WaitForLeadListReadyInput):
334
339
  targetLeadCount: number | undefined;
335
340
  warning: string | undefined;
336
341
  error: string | undefined;
342
+ sourceShortfall?: undefined;
337
343
  }>;
338
344
  export {};
@@ -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 returns source_under_capacity instead of ready.",
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.",
105
105
  },
106
106
  timeoutMs: {
107
107
  type: "number",
@@ -472,23 +472,12 @@ export async function waitForLeadListReady(input) {
472
472
  }
473
473
  }
474
474
  if ((!requireRows || rowCount > 0) && importComplete) {
475
- if (provider === "signal-discovery" &&
476
- typeof targetLeadCount === "number" &&
475
+ const signalSourceShortfallTarget = provider === "signal-discovery" && typeof targetLeadCount === "number"
476
+ ? targetLeadCount
477
+ : null;
478
+ const signalSourceShortfall = signalSourceShortfallTarget !== null &&
477
479
  rowCount > 0 &&
478
- rowCount < targetLeadCount) {
479
- return {
480
- ready: false,
481
- reason: "source_under_capacity",
482
- leadListId,
483
- provider: provider ?? null,
484
- attempts,
485
- elapsedMs: Date.now() - start,
486
- rowCount,
487
- status: lastStatus,
488
- targetLeadCount,
489
- error: `Signal Discovery completed with ${rowCount.toLocaleString("en-US")} source candidates, below the approved ${targetLeadCount.toLocaleString("en-US")} source-candidate target. Do not confirm this lead list; select more posts, rerun source discovery, or move to Sales Nav/Prospeo.`,
490
- };
491
- }
480
+ rowCount < signalSourceShortfallTarget;
492
481
  return {
493
482
  ready: true,
494
483
  leadListId,
@@ -498,6 +487,10 @@ export async function waitForLeadListReady(input) {
498
487
  rowCount,
499
488
  status: lastStatus,
500
489
  targetLeadCount: targetLeadCount ?? null,
490
+ sourceShortfall: signalSourceShortfall,
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.`
493
+ : undefined,
501
494
  };
502
495
  }
503
496
  }