@sellable/mcp 0.1.148 → 0.1.149

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/dist/index-dev.js CHANGED
File without changes
package/dist/index.js CHANGED
File without changes
@@ -9,6 +9,9 @@ export declare function selectSignalPostsForImport<T extends SignalPostForImport
9
9
  }): {
10
10
  posts: T[];
11
11
  estimatedEngagers: number;
12
+ availableEngagers: number;
13
+ targetEngagerCount: number | null;
14
+ targetReached: boolean;
12
15
  limited: boolean;
13
16
  };
14
17
  export type GetProviderPromptInput = {
@@ -343,13 +343,18 @@ export function selectSignalPostsForImport(posts, options) {
343
343
  const normalizedTargetEngagers = normalizePositiveInteger(options.targetEngagerCount);
344
344
  const normalizedMaxPosts = normalizePositiveInteger(options.maxPostsToScrape);
345
345
  if (!normalizedTargetEngagers && !normalizedMaxPosts) {
346
+ const availableEngagers = posts.reduce((sum, post) => sum + post.likes + post.comments, 0);
346
347
  return {
347
348
  posts,
348
- estimatedEngagers: posts.reduce((sum, post) => sum + post.likes + post.comments, 0),
349
+ estimatedEngagers: availableEngagers,
350
+ availableEngagers,
351
+ targetEngagerCount: null,
352
+ targetReached: true,
349
353
  limited: false,
350
354
  };
351
355
  }
352
356
  const ranked = [...posts].sort((a, b) => b.likes + b.comments - (a.likes + a.comments));
357
+ const availableEngagers = ranked.reduce((sum, post) => sum + post.likes + post.comments, 0);
353
358
  const selected = [];
354
359
  let estimatedEngagers = 0;
355
360
  for (const post of ranked) {
@@ -366,6 +371,10 @@ export function selectSignalPostsForImport(posts, options) {
366
371
  return {
367
372
  posts: selected.length > 0 ? selected : ranked,
368
373
  estimatedEngagers,
374
+ availableEngagers,
375
+ targetEngagerCount: normalizedTargetEngagers ?? null,
376
+ targetReached: !normalizedTargetEngagers ||
377
+ estimatedEngagers >= normalizedTargetEngagers,
369
378
  limited: selected.length > 0 && selected.length < posts.length,
370
379
  };
371
380
  }
@@ -2312,12 +2321,19 @@ export async function importLeads(input) {
2312
2321
  maxPostsToScrape,
2313
2322
  });
2314
2323
  const postsToScrape = importSelection.posts;
2324
+ if (!importSelection.targetReached && importSelection.targetEngagerCount) {
2325
+ const capClause = importSelection.availableEngagers >= importSelection.targetEngagerCount
2326
+ ? ` The selected posts can cover the target, but maxPostsToScrape=${normalizePositiveInteger(maxPostsToScrape)} prevents reaching it. Increase maxPostsToScrape or remove that cap.`
2327
+ : " Select/promote more right-content posts, run another narrow Signal Discovery search, or switch to Sales Nav recent activity if the lane cannot produce enough source candidates.";
2328
+ throw new Error(`Signal Discovery selected posts only cover about ${importSelection.estimatedEngagers.toLocaleString("en-US")} visible engagers, below the approved ${importSelection.targetEngagerCount.toLocaleString("en-US")} source-candidate target. Do not scrape this under-capacity post set.${capClause}`);
2329
+ }
2315
2330
  const effectiveHeadlineICPCriteria = headlineICPCriteria && headlineICPCriteria.length > 0
2316
2331
  ? headlineICPCriteria
2317
2332
  : rubricGuidelines;
2318
2333
  // Start the scrape job
2319
2334
  const result = await api.post(`/api/v3/campaigns/${campaignOfferId}/signal-leads/create`, {
2320
2335
  posts: postsToScrape,
2336
+ targetEngagerCount: importSelection.targetEngagerCount ?? undefined,
2321
2337
  ...(effectiveHeadlineICPCriteria &&
2322
2338
  effectiveHeadlineICPCriteria.length > 0
2323
2339
  ? {
@@ -2577,6 +2593,19 @@ export async function confirmLeadList(input) {
2577
2593
  const leadListConfig = leadListMeta.table?.config ?? null;
2578
2594
  const leadListRowCount = leadListMeta.rowCount ?? 0;
2579
2595
  const importProgress = leadListConfig?.importProgress ?? null;
2596
+ const signalSourceTargetLeadCount = resolvedProvider === "signal-discovery" &&
2597
+ typeof leadListConfig?.targetLeadCount === "number" &&
2598
+ Number.isFinite(leadListConfig.targetLeadCount) &&
2599
+ leadListConfig.targetLeadCount > 0
2600
+ ? leadListConfig.targetLeadCount
2601
+ : null;
2602
+ if (resolvedProvider === "signal-discovery" &&
2603
+ signalSourceTargetLeadCount !== null &&
2604
+ leadListConfig?.importStatus === "complete" &&
2605
+ leadListRowCount > 0 &&
2606
+ leadListRowCount < signalSourceTargetLeadCount) {
2607
+ 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.`);
2608
+ }
2580
2609
  const progressProcessed = typeof importProgress?.processed === "number"
2581
2610
  ? importProgress.processed
2582
2611
  : null;
@@ -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.",
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.",
105
105
  },
106
106
  timeoutMs: {
107
107
  type: "number",
@@ -472,6 +472,23 @@ 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" &&
477
+ 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
+ }
475
492
  return {
476
493
  ready: true,
477
494
  leadListId,
@@ -3702,7 +3702,6 @@ export declare const allTools: ({
3702
3702
  tableId?: undefined;
3703
3703
  targetCount?: undefined;
3704
3704
  minPassedCount?: undefined;
3705
- minMessagesCount?: undefined;
3706
3705
  timeoutMs?: undefined;
3707
3706
  intervalMs?: undefined;
3708
3707
  includeRows?: undefined;
@@ -3736,7 +3735,6 @@ export declare const allTools: ({
3736
3735
  tableId?: undefined;
3737
3736
  targetCount?: undefined;
3738
3737
  minPassedCount?: undefined;
3739
- minMessagesCount?: undefined;
3740
3738
  timeoutMs?: undefined;
3741
3739
  intervalMs?: undefined;
3742
3740
  includeRows?: undefined;
@@ -3795,7 +3793,6 @@ export declare const allTools: ({
3795
3793
  tableId?: undefined;
3796
3794
  targetCount?: undefined;
3797
3795
  minPassedCount?: undefined;
3798
- minMessagesCount?: undefined;
3799
3796
  timeoutMs?: undefined;
3800
3797
  intervalMs?: undefined;
3801
3798
  includeRows?: undefined;
@@ -3854,7 +3851,6 @@ export declare const allTools: ({
3854
3851
  tableId?: undefined;
3855
3852
  targetCount?: undefined;
3856
3853
  minPassedCount?: undefined;
3857
- minMessagesCount?: undefined;
3858
3854
  timeoutMs?: undefined;
3859
3855
  intervalMs?: undefined;
3860
3856
  includeRows?: undefined;
@@ -3915,7 +3911,6 @@ export declare const allTools: ({
3915
3911
  tableId?: undefined;
3916
3912
  targetCount?: undefined;
3917
3913
  minPassedCount?: undefined;
3918
- minMessagesCount?: undefined;
3919
3914
  timeoutMs?: undefined;
3920
3915
  intervalMs?: undefined;
3921
3916
  includeRows?: undefined;
@@ -3946,7 +3941,6 @@ export declare const allTools: ({
3946
3941
  tableId?: undefined;
3947
3942
  targetCount?: undefined;
3948
3943
  minPassedCount?: undefined;
3949
- minMessagesCount?: undefined;
3950
3944
  timeoutMs?: undefined;
3951
3945
  intervalMs?: undefined;
3952
3946
  includeRows?: undefined;
@@ -3977,7 +3971,6 @@ export declare const allTools: ({
3977
3971
  tableId?: undefined;
3978
3972
  targetCount?: undefined;
3979
3973
  minPassedCount?: undefined;
3980
- minMessagesCount?: undefined;
3981
3974
  timeoutMs?: undefined;
3982
3975
  intervalMs?: undefined;
3983
3976
  includeRows?: undefined;
@@ -33,7 +33,6 @@ type WaitForRubricResultsInput = {
33
33
  tableId?: string;
34
34
  targetCount?: number;
35
35
  minPassedCount?: number;
36
- minMessagesCount?: number;
37
36
  timeoutMs?: number;
38
37
  intervalMs?: number;
39
38
  includeRows?: boolean;
@@ -115,7 +114,6 @@ export declare const rubricToolDefinitions: ({
115
114
  tableId?: undefined;
116
115
  targetCount?: undefined;
117
116
  minPassedCount?: undefined;
118
- minMessagesCount?: undefined;
119
117
  timeoutMs?: undefined;
120
118
  intervalMs?: undefined;
121
119
  includeRows?: undefined;
@@ -149,7 +147,6 @@ export declare const rubricToolDefinitions: ({
149
147
  tableId?: undefined;
150
148
  targetCount?: undefined;
151
149
  minPassedCount?: undefined;
152
- minMessagesCount?: undefined;
153
150
  timeoutMs?: undefined;
154
151
  intervalMs?: undefined;
155
152
  includeRows?: undefined;
@@ -208,7 +205,6 @@ export declare const rubricToolDefinitions: ({
208
205
  tableId?: undefined;
209
206
  targetCount?: undefined;
210
207
  minPassedCount?: undefined;
211
- minMessagesCount?: undefined;
212
208
  timeoutMs?: undefined;
213
209
  intervalMs?: undefined;
214
210
  includeRows?: undefined;
@@ -267,7 +263,6 @@ export declare const rubricToolDefinitions: ({
267
263
  tableId?: undefined;
268
264
  targetCount?: undefined;
269
265
  minPassedCount?: undefined;
270
- minMessagesCount?: undefined;
271
266
  timeoutMs?: undefined;
272
267
  intervalMs?: undefined;
273
268
  includeRows?: undefined;
@@ -328,7 +323,6 @@ export declare const rubricToolDefinitions: ({
328
323
  tableId?: undefined;
329
324
  targetCount?: undefined;
330
325
  minPassedCount?: undefined;
331
- minMessagesCount?: undefined;
332
326
  timeoutMs?: undefined;
333
327
  intervalMs?: undefined;
334
328
  includeRows?: undefined;
@@ -359,7 +353,6 @@ export declare const rubricToolDefinitions: ({
359
353
  tableId?: undefined;
360
354
  targetCount?: undefined;
361
355
  minPassedCount?: undefined;
362
- minMessagesCount?: undefined;
363
356
  timeoutMs?: undefined;
364
357
  intervalMs?: undefined;
365
358
  includeRows?: undefined;
@@ -390,7 +383,6 @@ export declare const rubricToolDefinitions: ({
390
383
  tableId?: undefined;
391
384
  targetCount?: undefined;
392
385
  minPassedCount?: undefined;
393
- minMessagesCount?: undefined;
394
386
  timeoutMs?: undefined;
395
387
  intervalMs?: undefined;
396
388
  includeRows?: undefined;
@@ -420,10 +412,6 @@ export declare const rubricToolDefinitions: ({
420
412
  type: string;
421
413
  description: string;
422
414
  };
423
- minMessagesCount: {
424
- type: string;
425
- description: string;
426
- };
427
415
  timeoutMs: {
428
416
  type: string;
429
417
  description: string;
@@ -520,12 +508,6 @@ export declare function checkRubric(input: CheckRubricInput): Promise<{
520
508
  export declare function waitForRubricResults(input: WaitForRubricResultsInput): Promise<{
521
509
  stats: WorkflowTableStats;
522
510
  rows?: import("./rows.js").LightweightRow[] | undefined;
523
- messageGeneration?: {
524
- completed: number;
525
- passingGeneratedMessages: number;
526
- minMessagesCount: number;
527
- floorMet: boolean;
528
- } | undefined;
529
511
  ready: boolean;
530
512
  attempts: number;
531
513
  elapsedMs: number;
@@ -538,14 +520,28 @@ export declare function waitForRubricResults(input: WaitForRubricResultsInput):
538
520
  targetCount: number;
539
521
  pending?: undefined;
540
522
  };
523
+ reason?: undefined;
524
+ partialResult?: undefined;
525
+ diagnostic?: undefined;
526
+ guidance?: undefined;
541
527
  } | {
542
528
  stats: WorkflowTableStats;
543
529
  rows?: import("./rows.js").LightweightRow[] | undefined;
530
+ ready: boolean;
531
+ partial: boolean;
532
+ reason: string;
533
+ attempts: number;
534
+ elapsedMs: number;
535
+ tableId: string;
536
+ passRate: {
537
+ completed: number;
538
+ passed: number;
539
+ pending: number;
540
+ percent: number;
541
+ targetCount: number;
542
+ minPassedCount: number;
543
+ };
544
544
  partialResult: {
545
- messagesCount?: number | undefined;
546
- passingGeneratedMessages?: number | undefined;
547
- minMessagesCount?: number | undefined;
548
- messageFloorMet?: boolean | undefined;
549
545
  completed: number;
550
546
  passed: number;
551
547
  pending: number;
@@ -555,32 +551,23 @@ export declare function waitForRubricResults(input: WaitForRubricResultsInput):
555
551
  enoughToDiagnose: boolean;
556
552
  floorMet: boolean;
557
553
  };
558
- messageGeneration?: {
559
- completed: number;
560
- passingGeneratedMessages: number;
561
- minMessagesCount: number;
562
- floorMet: boolean;
563
- } | undefined;
554
+ diagnostic?: undefined;
555
+ guidance?: undefined;
556
+ } | {
564
557
  ready: boolean;
565
- partial: boolean;
566
558
  reason: string;
567
559
  attempts: number;
568
560
  elapsedMs: number;
569
561
  tableId: string;
570
562
  passRate: {
563
+ minPassedCount?: number | undefined;
571
564
  completed: number;
572
565
  passed: number;
573
566
  pending: number;
574
567
  percent: number;
575
568
  targetCount: number;
576
- minPassedCount: number;
577
569
  };
578
- } | {
579
570
  partialResult: {
580
- messagesCount?: number | undefined;
581
- passingGeneratedMessages?: number | undefined;
582
- minMessagesCount?: number | undefined;
583
- messageFloorMet?: boolean | undefined;
584
571
  enoughToDiagnose: boolean;
585
572
  floorMet: boolean;
586
573
  minPassedCount?: number | undefined;
@@ -594,31 +581,12 @@ export declare function waitForRubricResults(input: WaitForRubricResultsInput):
594
581
  totalRows: number;
595
582
  enrichedCount: number | undefined;
596
583
  needsEnrichCount: number | undefined;
597
- messagesCount: number;
584
+ messagesCount: number | undefined;
598
585
  needsApprovalCount: number | undefined;
599
586
  processingCount: number | undefined;
600
587
  failedCount: number | undefined;
601
588
  };
602
589
  guidance: string;
603
590
  stats: WorkflowTableStats | null;
604
- messageGeneration?: {
605
- completed: number;
606
- passingGeneratedMessages: number;
607
- minMessagesCount: number;
608
- floorMet: boolean;
609
- } | undefined;
610
- ready: boolean;
611
- reason: string;
612
- attempts: number;
613
- elapsedMs: number;
614
- tableId: string;
615
- passRate: {
616
- minPassedCount?: number | undefined;
617
- completed: number;
618
- passed: number;
619
- pending: number;
620
- percent: number;
621
- targetCount: number;
622
- };
623
591
  }>;
624
592
  export {};
@@ -9,11 +9,6 @@ const DEFAULT_INTERVAL_MS = 2000;
9
9
  function sleep(ms) {
10
10
  return new Promise((resolve) => setTimeout(resolve, ms));
11
11
  }
12
- function countPassingGeneratedMessages(rowSnapshot) {
13
- if (!rowSnapshot?.rows?.length)
14
- return 0;
15
- return rowSnapshot.rows.filter((row) => row.icpPassed === true && Boolean(row.message?.trim())).length;
16
- }
17
12
  function normalizeRubricItemDefaults(item) {
18
13
  return {
19
14
  ...item,
@@ -398,10 +393,6 @@ export const rubricToolDefinitions = [
398
393
  type: "number",
399
394
  description: "Optional pass floor for bounded create-campaign samples. When this floor is met, the tool returns ready:true with partial:true instead of waiting for every target row to finish.",
400
395
  },
401
- minMessagesCount: {
402
- type: "number",
403
- description: "Optional generated-message floor. When provided with minPassedCount, the tool waits until both floors are met so create-campaign-v2 can review the first passing generated message without waiting for the full batch.",
404
- },
405
396
  timeoutMs: {
406
397
  type: "number",
407
398
  description: `Max time to wait in ms (default ${DEFAULT_TIMEOUT_MS}).`,
@@ -697,7 +688,6 @@ export async function waitForRubricResults(input) {
697
688
  const intervalMs = Math.max(500, input.intervalMs ?? DEFAULT_INTERVAL_MS);
698
689
  const targetCount = resolveMaxProspects(input.targetCount);
699
690
  const minPassedCount = resolveMinPassedCount(input.minPassedCount);
700
- const minMessagesCount = resolveMinPassedCount(input.minMessagesCount);
701
691
  const includeRows = input.includeRows !== false;
702
692
  let tableId = input.tableId;
703
693
  if (!tableId && input.campaignOfferId) {
@@ -719,33 +709,19 @@ export async function waitForRubricResults(input) {
719
709
  const totalRows = stats.totalRows ?? 0;
720
710
  const effectiveTarget = totalRows > 0 ? Math.min(targetCount, totalRows) : targetCount;
721
711
  const pending = Math.max(effectiveTarget - completed, 0);
722
- const messagesCount = stats.messagesCount ?? 0;
723
712
  const failedCount = stats.failedCount ?? 0;
724
713
  const processingCount = stats.processingCount ?? 0;
725
714
  const queuedCount = stats.queuedCount ?? 0;
726
715
  const cancellableCount = stats.cancellableCount ?? 0;
727
716
  const minPassFloorMet = minPassedCount !== null && passed >= minPassedCount;
728
- const rawMessageFloorMet = minMessagesCount === null || messagesCount >= minMessagesCount;
729
- let rowSnapshotForMessageCheck = null;
730
- let passingGeneratedMessagesCount = null;
731
- if (minMessagesCount !== null && minPassFloorMet && rawMessageFloorMet) {
732
- rowSnapshotForMessageCheck = await getTableRowsMinimal(tableId, {
733
- limit: effectiveTarget,
734
- });
735
- passingGeneratedMessagesCount = countPassingGeneratedMessages(rowSnapshotForMessageCheck);
736
- }
737
- const messageFloorMet = minMessagesCount === null ||
738
- (passingGeneratedMessagesCount ?? 0) >= minMessagesCount;
739
- const earlyFloorMet = minPassFloorMet && messageFloorMet;
740
717
  const unresolvedRowsResolvedAsFailures = pending > 0 && completed + failedCount >= effectiveTarget;
741
718
  const noActiveProcessing = processingCount === 0 && queuedCount === 0 && cancellableCount === 0;
742
- if (completed >= effectiveTarget && messageFloorMet) {
719
+ if (completed >= effectiveTarget) {
743
720
  const percent = completed > 0 ? Math.round((passed / completed) * 100) : 0;
744
721
  const rowSnapshot = includeRows
745
- ? (rowSnapshotForMessageCheck ??
746
- (await getTableRowsMinimal(tableId, {
747
- limit: effectiveTarget,
748
- })))
722
+ ? await getTableRowsMinimal(tableId, {
723
+ limit: effectiveTarget,
724
+ })
749
725
  : null;
750
726
  return {
751
727
  ready: true,
@@ -759,21 +735,11 @@ export async function waitForRubricResults(input) {
759
735
  targetCount: effectiveTarget,
760
736
  ...(minPassedCount !== null ? { minPassedCount } : {}),
761
737
  },
762
- ...(minMessagesCount !== null
763
- ? {
764
- messageGeneration: {
765
- completed: messagesCount,
766
- passingGeneratedMessages: passingGeneratedMessagesCount ?? 0,
767
- minMessagesCount,
768
- floorMet: true,
769
- },
770
- }
771
- : {}),
772
738
  ...(rowSnapshot ? { rows: rowSnapshot.rows } : {}),
773
739
  stats,
774
740
  };
775
741
  }
776
- if (earlyFloorMet) {
742
+ if (minPassFloorMet) {
777
743
  const percent = completed > 0 ? Math.round((passed / completed) * 100) : 0;
778
744
  const reason = !noActiveProcessing
779
745
  ? "min_passed_count_met_with_active_processing"
@@ -781,10 +747,9 @@ export async function waitForRubricResults(input) {
781
747
  ? "min_passed_count_met_with_resolved_failures"
782
748
  : "min_passed_count_met_no_active_processing";
783
749
  const rowSnapshot = includeRows
784
- ? (rowSnapshotForMessageCheck ??
785
- (await getTableRowsMinimal(tableId, {
786
- limit: effectiveTarget,
787
- })))
750
+ ? await getTableRowsMinimal(tableId, {
751
+ limit: effectiveTarget,
752
+ })
788
753
  : null;
789
754
  return {
790
755
  ready: true,
@@ -801,16 +766,6 @@ export async function waitForRubricResults(input) {
801
766
  targetCount: effectiveTarget,
802
767
  minPassedCount,
803
768
  },
804
- ...(minMessagesCount !== null
805
- ? {
806
- messageGeneration: {
807
- completed: messagesCount,
808
- passingGeneratedMessages: passingGeneratedMessagesCount ?? 0,
809
- minMessagesCount,
810
- floorMet: true,
811
- },
812
- }
813
- : {}),
814
769
  partialResult: {
815
770
  completed,
816
771
  passed,
@@ -820,14 +775,6 @@ export async function waitForRubricResults(input) {
820
775
  minPassedCount,
821
776
  enoughToDiagnose: true,
822
777
  floorMet: true,
823
- ...(minMessagesCount !== null
824
- ? {
825
- messagesCount,
826
- passingGeneratedMessages: passingGeneratedMessagesCount ?? 0,
827
- minMessagesCount,
828
- messageFloorMet: true,
829
- }
830
- : {}),
831
778
  },
832
779
  ...(rowSnapshot ? { rows: rowSnapshot.rows } : {}),
833
780
  stats,
@@ -838,21 +785,9 @@ export async function waitForRubricResults(input) {
838
785
  const completed = lastStats?.passRate?.completed ?? 0;
839
786
  const passed = lastStats?.passRate?.passed ?? 0;
840
787
  const percent = completed > 0 ? Math.round((passed / completed) * 100) : 0;
841
- const messagesCount = lastStats?.messagesCount ?? 0;
842
788
  const totalRows = lastStats?.totalRows ?? 0;
843
789
  const effectiveTarget = totalRows > 0 ? Math.min(targetCount, totalRows) : targetCount;
844
790
  const pending = Math.max(effectiveTarget - completed, 0);
845
- let timeoutPassingGeneratedMessagesCount = null;
846
- if (minMessagesCount !== null) {
847
- try {
848
- timeoutPassingGeneratedMessagesCount = countPassingGeneratedMessages(await getTableRowsMinimal(tableId, {
849
- limit: effectiveTarget,
850
- }));
851
- }
852
- catch {
853
- timeoutPassingGeneratedMessagesCount = null;
854
- }
855
- }
856
791
  return {
857
792
  ready: false,
858
793
  reason: "timeout",
@@ -867,16 +802,6 @@ export async function waitForRubricResults(input) {
867
802
  targetCount: effectiveTarget,
868
803
  ...(minPassedCount !== null ? { minPassedCount } : {}),
869
804
  },
870
- ...(minMessagesCount !== null
871
- ? {
872
- messageGeneration: {
873
- completed: messagesCount,
874
- passingGeneratedMessages: timeoutPassingGeneratedMessagesCount ?? 0,
875
- minMessagesCount,
876
- floorMet: (timeoutPassingGeneratedMessagesCount ?? 0) >= minMessagesCount,
877
- },
878
- }
879
- : {}),
880
805
  partialResult: {
881
806
  completed,
882
807
  passed,
@@ -886,25 +811,17 @@ export async function waitForRubricResults(input) {
886
811
  ...(minPassedCount !== null ? { minPassedCount } : {}),
887
812
  enoughToDiagnose: completed > 0,
888
813
  floorMet: minPassedCount !== null && passed >= minPassedCount,
889
- ...(minMessagesCount !== null
890
- ? {
891
- messagesCount,
892
- passingGeneratedMessages: timeoutPassingGeneratedMessagesCount ?? 0,
893
- minMessagesCount,
894
- messageFloorMet: (timeoutPassingGeneratedMessagesCount ?? 0) >= minMessagesCount,
895
- }
896
- : {}),
897
814
  },
898
815
  diagnostic: {
899
816
  totalRows,
900
817
  enrichedCount: lastStats?.enrichedCount,
901
818
  needsEnrichCount: lastStats?.needsEnrichCount,
902
- messagesCount,
819
+ messagesCount: lastStats?.messagesCount,
903
820
  needsApprovalCount: lastStats?.needsApprovalCount,
904
821
  processingCount: lastStats?.processingCount,
905
822
  failedCount: lastStats?.failedCount,
906
823
  },
907
- guidance: "If this is create-campaign-v2 validate-sample, do not repeat waits indefinitely. Use passRate/stats to diagnose and surface sample_revision_required before Settings when the sample is under the pass floor. If minMessagesCount is set, one passing generated message is enough to start review; do not wait for a stronger sample.",
824
+ guidance: "If this is create-campaign-v2 validate-sample, do not repeat waits indefinitely. Use passRate/stats to diagnose and surface sample_revision_required before Settings when the sample is under the pass floor or messages are incomplete.",
908
825
  stats: lastStats,
909
826
  };
910
827
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.148",
3
+ "version": "0.1.149",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -49,10 +49,9 @@ for debug output.
49
49
  12. Sync the approved template into the campaign brief.
50
50
  13. After message approval, keep the watched app on Filter Leads while the
51
51
  bounded enrichment/filter cascade starts.
52
- 14. Move to Messages only after at least one review row passes and one generated
53
- message is ready for review. Do not wait for a stronger sample once that
54
- first passing message exists; review it, then hand off to Settings, sender,
55
- sequence, and explicit launch greenlight.
52
+ 14. Move to Messages only after at least one review row passes, generate and
53
+ review bounded messages, then hand off to Settings, sender, sequence, and
54
+ explicit launch greenlight.
56
55
 
57
56
  There is no normal approval-packet, commit-gate, atomic-mint, or local
58
57
  artifact-validation step. Those belong only to legacy validation/rehearsal
@@ -1124,14 +1124,8 @@
1124
1124
  "cellSource": "pending_generate_message_cells_for_passing_rows using approved_campaign_brief_template"
1125
1125
  },
1126
1126
  {
1127
- "tool": "wait_for_rubric_results",
1128
- "purpose": "wait_for_first_passing_generated_message",
1129
- "requiredValues": {
1130
- "includeRows": false,
1131
- "minPassedCount": 1,
1132
- "minMessagesCount": 1
1133
- },
1134
- "readVia": "stats_only_tool_result"
1127
+ "tool": "wait_for_campaign_table_ready",
1128
+ "purpose": "wait_for_generate_message_cells_to_complete"
1135
1129
  },
1136
1130
  {
1137
1131
  "action": "observe_generate_message_results",
@@ -1175,24 +1169,25 @@
1175
1169
  "currentStep": "auto-execute-messaging",
1176
1170
  "watchNarration.stage": "review-ready"
1177
1171
  },
1178
- "watchNarrationRule": "Say the first passing generated message is ready in Messages; next is review before Settings."
1172
+ "watchNarrationRule": "Say what just happened, that review-batch messages are ready in Messages, and that the next action is reviewing or approving generated messages before Settings."
1179
1173
  },
1180
1174
  {
1181
1175
  "action": "ask_generated_message_review_choice",
1182
1176
  "uses": "request_user_input",
1183
1177
  "choices": [
1184
- "Approve generated message and continue to Settings",
1178
+ "Approve generated messages",
1185
1179
  "Revise filters",
1186
1180
  "Revise message template",
1187
1181
  "Pause here"
1188
- ]
1182
+ ],
1183
+ "rule": "Do not advance to Settings until the generated review-batch messages have been reviewed and approved."
1189
1184
  }
1190
1185
  ],
1191
1186
  "allowedTools": [
1192
1187
  "get_subskill_asset",
1193
1188
  "get_rows_minimal",
1194
1189
  "queue_cells",
1195
- "wait_for_rubric_results",
1190
+ "wait_for_campaign_table_ready",
1196
1191
  "update_campaign",
1197
1192
  "AskUserQuestion",
1198
1193
  "request_user_input"
@@ -1206,7 +1201,6 @@
1206
1201
  "hardRules": [
1207
1202
  "critique_failure_never_escalates",
1208
1203
  "critique_sample_size_bounded_by_config",
1209
- "first_passing_generated_message_unblocks_review",
1210
1204
  "critics_fixed_at_targeting_copy_voice",
1211
1205
  "synthesis_enforces_phase_84_token_contract",
1212
1206
  "opus_reserved_for_highest_value_subset",
@@ -60,10 +60,9 @@ auto-revise leads.
60
60
  explicit batch count anyway so future larger expansion batches do not
61
61
  accidentally stop early
62
62
  (see §Known Tool Behaviors #3)
63
- - minPassedCount=1 means one passing filtered row unblocks Step 15 Generate
64
- Message observation. Step 15 then waits only for one generated message
65
- (`minMessagesCount=1`) before review. Do not wait for all sample rows to
66
- finish before messages start.
63
+ - minPassedCount=1 means one passing filtered row unblocks Step 15
64
+ Generate Message observation. Do not wait for all sample rows to finish
65
+ before messages start.
67
66
 
68
67
  7. call `wait_for_rubric_results` with `includeRows=false`; extract ONLY:
69
68
  - ready: boolean
@@ -102,9 +101,7 @@ auto-revise leads.
102
101
  10. branch:
103
102
  if passInSample >= 1:
104
103
  proceed to Step 15 (auto-execute-messaging) with currently passing rows
105
- so Generate Message can start without waiting for the full sample. Once
106
- one passing generated message is ready, stop for user review instead of
107
- waiting for a stronger sample.
104
+ so Generate Message can start without waiting for the full sample
108
105
  else:
109
106
  diagnose (see Brief-vs-List Diagnosis below)
110
107
  revisionRound += 1
@@ -182,8 +179,7 @@ processing makes the experience feel frozen.
182
179
 
183
180
  Workaround: treat timeout stats as a partial sample. If at least one row has
184
181
  passed, move to Step 15 and observe or queue Generate Message for the passing
185
- rows; Step 15 stops when one generated message is ready. If zero rows have
186
- passed and no active processing is visible, stop at
182
+ rows. If zero rows have passed and no active processing is visible, stop at
187
183
  `Status: sample-needs-revision` before Settings. Show the completed / passed /
188
184
  pending counts and ask whether to revise source, revise filter/rubric, or wait
189
185
  once only if active processing is still visible.
@@ -127,9 +127,11 @@ rate with a conservative cleanup factor, cap the source candidates at the
127
127
  approved provider limit, and select only enough posts to reach that engager
128
128
  count. The planning floor is 10% projected fit after cleanup; if Signal
129
129
  Discovery falls below that floor, do not import the source list. Route back to
130
- find-leads and move to Sales Nav recent activity instead. The subsequent
131
- `confirm_lead_list` call still uses `targetLeadCount: <importLimit>` so only the
132
- bounded review batch enters the campaign table.
130
+ find-leads and move to Sales Nav recent activity instead. After the scrape,
131
+ `wait_for_lead_list_ready` must clear the approved source-candidate target; if
132
+ it reports `source_under_capacity`, do not call `confirm_lead_list`. The
133
+ subsequent `confirm_lead_list` call still uses `targetLeadCount: <importLimit>`
134
+ so only the bounded review batch enters the campaign table.
133
135
 
134
136
  For supplied direct lists, `confirm_lead_list` must receive
135
137
  `targetLeadCount: <importLimit>` or explicit `sourceRowIds` so a 100-row source
@@ -4,16 +4,15 @@ This reference governs Step 15 (`auto-execute-messaging`) re-cascade
4
4
  behavior when Step 14's validate-sample loop graduates additional rows
5
5
  from pending → passed after the first Generate Message cells have already run.
6
6
 
7
- Load whenever Step 15 is about to ask for generated-message review or transition
8
- to `awaiting-user-greenlight`, and on every resume into Step 15.
7
+ Load whenever Step 15 is about to transition to
8
+ `awaiting-user-greenlight`, and on every resume into Step 15.
9
9
 
10
10
  ## Principle
11
11
 
12
- Generate Message cells are cascade-scoped. If Step 14's rubric flips rows from
13
- pending → passed AFTER Step 15 first observes messages for the review batch, the
14
- new rows can sit pending with no message. That must not block the first review
15
- handoff. Step 15 opens review as soon as one passing generated message exists;
16
- late-passed rows can be re-cascaded after explicit user continuation or resume.
12
+ Generate Message cells are cascade-scoped. If Step 14's rubric flips rows
13
+ from pending → passed AFTER Step 15 first observes messages for the review
14
+ batch, the new rows can sit pending with no message. Step 15 must explicitly
15
+ queue the newly-pending Generate Message cells for the review-batch subset.
17
16
 
18
17
  Observed on manual Phase-85 signal-discovery run (2026-04-20): 15 new
19
18
  rows graduated pending → passed mid-tail while `messagesCount` stayed
@@ -23,17 +22,13 @@ flat at 257. The new rows never got messages.
23
22
 
24
23
  Re-cascade runs whenever ALL of the following are true:
25
24
 
26
- 1. The user has already approved the first generated message or explicitly asked
27
- to continue processing more review-batch rows.
25
+ 1. Step 15 has already observed the initial review-batch message cascade.
28
26
  2. A subsequent check of rubric state shows rows that were pending at
29
27
  first message-generation pass are now passed.
30
28
  3. Those newly-passed rows do NOT yet have generated messages
31
29
  (`messagesCount` flat relative to pre-cascade).
32
30
  4. Step 15 has NOT yet transitioned to `awaiting-user-greenlight`.
33
31
 
34
- Before that first generated-message approval, do not run this loop just to wait
35
- for a bigger sample. One passing generated message is enough for review.
36
-
37
32
  If condition 4 already fired (i.e. we're in Step 16), the re-cascade
38
33
  runs on resume into Step 15 if validate-sample was re-entered and new
39
34
  rows graduated.
@@ -252,17 +252,16 @@ Template approved, bounded filter test running:
252
252
  "stage": "fit-message",
253
253
  "headline": "Template saved",
254
254
  "visibleState": "The browser stays on Filter Leads while the bounded enrichment and filter test runs.",
255
- "agentIntent": "Codex saved the approved message template, queued the review-batch Enrich Prospect cells, and is waiting for one passing row with one generated message before moving to review.",
256
- "nextAction": "Review the first passing generated message"
255
+ "agentIntent": "Codex saved the approved message template, queued the review-batch Enrich Prospect cells, and is waiting for at least one row to pass before moving to Messages.",
256
+ "nextAction": "Move to Messages after a passing row"
257
257
  }
258
258
  ```
259
259
 
260
260
  Do not move to Messages immediately after `approve-message`. The visible route
261
261
  is already Filter Leads after `save_rubrics`; approving the message only unlocks
262
262
  the bounded cascade from that screen. Move to Messages only once at least one
263
- review-batch row passes and one generated message is ready for review. Do not
264
- wait for the rest of the review batch or a stronger sample before asking the
265
- user to approve the generated message.
263
+ review-batch row passes and Generate Message cells are ready/running for the
264
+ passing rows.
266
265
 
267
266
  Messages waiting for template:
268
267
 
@@ -58,12 +58,11 @@ Step 14 — kick bounded cascade + observe sample
58
58
  passing filtered row exists.)
59
59
 
60
60
  Step 15 — observe messaging
61
- wait_for_rubric_results({ minPassedCount: 1, minMessagesCount: 1, includeRows: false })
62
- get_rows_minimal # confirm the first passing generated message exists
61
+ get_rows_minimal # confirm passing rows have completed Generate Message cells
63
62
  (rare) queue_cells on any pending Generate Message cells
64
63
  token-contract spot check via get_rows
65
64
  update_campaign(currentStep=auto-execute-messaging) with review-ready narration
66
- ask the user to approve the generated message before Settings
65
+ ask the user to approve generated review-batch messages before Settings
67
66
  only after approval: update_campaign(currentStep=awaiting-user-greenlight)
68
67
  (generate_messages is NOT an MCP tool; messages come from the cascade)
69
68
 
@@ -292,10 +291,8 @@ Do not route to a visible `validate-sample` step. Full decision tree lives in
292
291
  review batch only. After `save_rubrics` and the approved message template are
293
292
  persisted, Step 14 queues the review-batch Enrich Prospect cells, waits until
294
293
  filter results start landing, then moves to message observation as soon as one
295
- row passes. Step 15 opens review as soon as one passing generated message
296
- exists. Do not wait for a larger or stronger sample once that first passing
297
- message is ready. It does NOT call `check_rubric`,
298
- `bulk_enrich_with_prospeo`, or any other direct enrichment/scoring tool.
294
+ row passes. It does NOT call `check_rubric`, `bulk_enrich_with_prospeo`, or any
295
+ other direct enrichment/scoring tool.
299
296
 
300
297
  Shape:
301
298
 
@@ -310,7 +307,6 @@ projectedPass = round(passInSample / sampleSize * importLimit)
310
307
  if wait_for_rubric_results.ready === true and passRate.passed >= 1:
311
308
  advance to Step 15 to observe or queue Generate Message for currently passing rows
312
309
  do not wait for every sample row to finish before message generation starts
313
- stop for review as soon as one passing generated message is ready
314
310
  else if wait_for_rubric_results.ready === false and reason === "timeout":
315
311
  use the partial passRate/stats as the sample diagnostic
316
312
  if passRate.passed >= 1:
@@ -386,20 +382,17 @@ Template`. If it does not, fail before the cascade runs. Do not repair
386
382
  already (cascade auto-fired it). If it is still `pending`, queue
387
383
  it explicitly: `queue_cells({ tableId, cellIds:
388
384
  <generateMessageCellIds> })`.
389
- 3. `wait_for_rubric_results({ tableId, targetCount: reviewBatchSize,
390
- minPassedCount: 1, minMessagesCount: 1, includeRows: false })` until the
391
- first passing generated message is ready. Do not wait for every passing row's
392
- Generate Message cell, and do not add "one more wait" for a stronger sample.
393
- Remaining review-batch rows can continue processing in the background.
394
- 4. Read the first ready generated message back via `get_rows` (full) and
395
- sanity-check that sample against the Phase 84 token contract: no unresolved
385
+ 3. `wait_for_campaign_table_ready` (or poll) until every passing
386
+ row's Generate Message cell is `completed`.
387
+ 4. Read the results back via `get_rows` (full) and sanity-check a
388
+ sample against the Phase 84 token contract: no unresolved
396
389
  `{{tokens}}`, no invented proof, one sentence per line, etc.
397
390
  5. If the sample fails the token contract, diagnose brief-vs-list
398
391
  (same revision loop as Step 14) and escalate if over
399
392
  `maxRevisionRounds`.
400
- 6. On success, keep `currentStep: "auto-execute-messaging"` and ask the user to
401
- approve the generated message before continuing to Settings. Only that
402
- approval may move the campaign to Settings / `awaiting-user-greenlight`.
393
+ 6. On success, keep `currentStep: "auto-execute-messaging"` and ask the user
394
+ to approve the generated review-batch messages. Only that approval may move
395
+ the campaign to Settings / `awaiting-user-greenlight`.
403
396
 
404
397
  **Do NOT hand-write message bodies via `update_cell`.** `update_cell`
405
398
  is reachable for legitimate operator overrides AFTER the tail hands
@@ -439,16 +432,15 @@ strings, which is why Step 16 requires Step 15 to be complete.
439
432
  4. Check `messaging.tokenContract`. When `strict`, reject any sample
440
433
  message that contains unresolved or unsupported tokens (including
441
434
  any critique rewrite that tried to introduce one).
442
- 5. If the first passing generated message passes the token contract (and
443
- critique when enabled), stop at the review-batch handoff. Do NOT wait for the
444
- remaining review-batch rows and do NOT scale to the full source list before
445
- explicit user expansion approval.
435
+ 5. If the sample passes the token contract (and critique when enabled),
436
+ stop at the review-batch handoff. Do NOT scale to the full source list
437
+ before explicit user expansion approval.
446
438
  6. If the sample fails the token contract or critique, diagnose +
447
439
  loop the same way Step 14 does (brief-vs-list), subject to the same
448
440
  `maxRevisionRounds` cap.
449
441
  7. On success, keep `currentStep: "auto-execute-messaging"`, show that the
450
- first passing generated message is ready in Messages, and ask for approval
451
- before Settings. Only after the user approves the generated message should
442
+ review-batch messages are ready in Messages, and ask for approval before
443
+ Settings. Only after the user approves those generated messages should
452
444
  `update_campaign({ campaignId, currentStep: "awaiting-user-greenlight" })`
453
445
  run.
454
446
 
@@ -459,6 +459,9 @@ targetLeadCount, targetEngagerCount, maxPostsToScrape })` for the approved
459
459
  source-capacity plan without asking for another yes/no gate. Then
460
460
  `confirm_lead_list` imports only the bounded review batch into the campaign
461
461
  table. Do not confuse the source-candidate target with the review-batch size.
462
+ If the completed source scrape comes back below the approved source-candidate
463
+ target, do not call `confirm_lead_list`; select more posts, rerun source
464
+ discovery, or move to Sales Nav.
462
465
 
463
466
  The promotion/select step is required campaign state. Use post IDs from the
464
467
  current campaign-scoped search result, not stale IDs copied from a source review
@@ -1,9 +0,0 @@
1
- {
2
- "parallelMode": "wide",
3
- "agentCount": 6,
4
- "maxToolCallsPerAgent": 2,
5
- "senderMaxAgents": 2,
6
- "senderMaxToolCallsPerAgent": 3,
7
- "progressMode": true,
8
- "debugMode": true
9
- }