@sellable/mcp 0.1.105 → 0.1.107

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.
@@ -2366,6 +2366,15 @@ export async function selectPromisingPosts(input) {
2366
2366
  headlineICPCriteria,
2367
2367
  rubricGuidelines: headlineICPCriteria,
2368
2368
  });
2369
+ if (selectionResult.selectedCount <= 0) {
2370
+ return {
2371
+ success: false,
2372
+ selectedCount: selectionResult.selectedCount,
2373
+ unselectedCount: selectionResult.unselectedCount,
2374
+ criteriaCount: selectionResult.criteriaCount,
2375
+ message: "No Signal Discovery posts were selected for this campaign. Do not import yet. Re-run a campaign-scoped search_signals call, use recommendedPostIds from the current campaign search state, or ask the user to promote posts in the UI before retrying select_promising_posts.",
2376
+ };
2377
+ }
2369
2378
  // Update currentStep if provided (via v2 endpoint)
2370
2379
  if (currentStep) {
2371
2380
  await api.put(`/api/v2/campaign-offers/${campaignOfferId}`, {
@@ -32,6 +32,7 @@ type WaitForRubricResultsInput = {
32
32
  campaignOfferId?: string;
33
33
  tableId?: string;
34
34
  targetCount?: number;
35
+ minPassedCount?: number;
35
36
  timeoutMs?: number;
36
37
  intervalMs?: number;
37
38
  includeRows?: boolean;
@@ -44,6 +45,9 @@ type WorkflowTableStats = {
44
45
  messagesCount?: number;
45
46
  needsApprovalCount?: number;
46
47
  processingCount?: number;
48
+ queuedCount?: number;
49
+ cancellableCount?: number;
50
+ readyEnrichCount?: number;
47
51
  failedCount?: number;
48
52
  passRate?: {
49
53
  completed: number;
@@ -109,6 +113,7 @@ export declare const rubricToolDefinitions: ({
109
113
  maxProspects?: undefined;
110
114
  tableId?: undefined;
111
115
  targetCount?: undefined;
116
+ minPassedCount?: undefined;
112
117
  timeoutMs?: undefined;
113
118
  intervalMs?: undefined;
114
119
  includeRows?: undefined;
@@ -141,6 +146,7 @@ export declare const rubricToolDefinitions: ({
141
146
  maxProspects?: undefined;
142
147
  tableId?: undefined;
143
148
  targetCount?: undefined;
149
+ minPassedCount?: undefined;
144
150
  timeoutMs?: undefined;
145
151
  intervalMs?: undefined;
146
152
  includeRows?: undefined;
@@ -198,6 +204,7 @@ export declare const rubricToolDefinitions: ({
198
204
  maxProspects?: undefined;
199
205
  tableId?: undefined;
200
206
  targetCount?: undefined;
207
+ minPassedCount?: undefined;
201
208
  timeoutMs?: undefined;
202
209
  intervalMs?: undefined;
203
210
  includeRows?: undefined;
@@ -255,6 +262,7 @@ export declare const rubricToolDefinitions: ({
255
262
  maxProspects?: undefined;
256
263
  tableId?: undefined;
257
264
  targetCount?: undefined;
265
+ minPassedCount?: undefined;
258
266
  timeoutMs?: undefined;
259
267
  intervalMs?: undefined;
260
268
  includeRows?: undefined;
@@ -314,6 +322,7 @@ export declare const rubricToolDefinitions: ({
314
322
  maxProspects?: undefined;
315
323
  tableId?: undefined;
316
324
  targetCount?: undefined;
325
+ minPassedCount?: undefined;
317
326
  timeoutMs?: undefined;
318
327
  intervalMs?: undefined;
319
328
  includeRows?: undefined;
@@ -343,6 +352,7 @@ export declare const rubricToolDefinitions: ({
343
352
  maxProspects?: undefined;
344
353
  tableId?: undefined;
345
354
  targetCount?: undefined;
355
+ minPassedCount?: undefined;
346
356
  timeoutMs?: undefined;
347
357
  intervalMs?: undefined;
348
358
  includeRows?: undefined;
@@ -372,6 +382,7 @@ export declare const rubricToolDefinitions: ({
372
382
  checkName?: undefined;
373
383
  tableId?: undefined;
374
384
  targetCount?: undefined;
385
+ minPassedCount?: undefined;
375
386
  timeoutMs?: undefined;
376
387
  intervalMs?: undefined;
377
388
  includeRows?: undefined;
@@ -397,6 +408,10 @@ export declare const rubricToolDefinitions: ({
397
408
  type: string;
398
409
  description: string;
399
410
  };
411
+ minPassedCount: {
412
+ type: string;
413
+ description: string;
414
+ };
400
415
  timeoutMs: {
401
416
  type: string;
402
417
  description: string;
@@ -496,6 +511,7 @@ export declare function waitForRubricResults(input: WaitForRubricResultsInput):
496
511
  elapsedMs: number;
497
512
  tableId: string;
498
513
  passRate: {
514
+ minPassedCount?: number | undefined;
499
515
  completed: number;
500
516
  passed: number;
501
517
  percent: number;
@@ -507,7 +523,10 @@ export declare function waitForRubricResults(input: WaitForRubricResultsInput):
507
523
  diagnostic?: undefined;
508
524
  guidance?: undefined;
509
525
  } | {
526
+ stats: WorkflowTableStats;
527
+ rows?: import("./rows.js").LightweightRow[] | undefined;
510
528
  ready: boolean;
529
+ partial: boolean;
511
530
  reason: string;
512
531
  attempts: number;
513
532
  elapsedMs: number;
@@ -518,6 +537,7 @@ export declare function waitForRubricResults(input: WaitForRubricResultsInput):
518
537
  pending: number;
519
538
  percent: number;
520
539
  targetCount: number;
540
+ minPassedCount: number;
521
541
  };
522
542
  partialResult: {
523
543
  completed: number;
@@ -525,7 +545,35 @@ export declare function waitForRubricResults(input: WaitForRubricResultsInput):
525
545
  pending: number;
526
546
  percent: number;
527
547
  targetCount: number;
548
+ minPassedCount: number;
549
+ enoughToDiagnose: boolean;
550
+ floorMet: boolean;
551
+ };
552
+ diagnostic?: undefined;
553
+ guidance?: undefined;
554
+ } | {
555
+ ready: boolean;
556
+ reason: string;
557
+ attempts: number;
558
+ elapsedMs: number;
559
+ tableId: string;
560
+ passRate: {
561
+ minPassedCount?: number | undefined;
562
+ completed: number;
563
+ passed: number;
564
+ pending: number;
565
+ percent: number;
566
+ targetCount: number;
567
+ };
568
+ partialResult: {
528
569
  enoughToDiagnose: boolean;
570
+ floorMet: boolean;
571
+ minPassedCount?: number | undefined;
572
+ completed: number;
573
+ passed: number;
574
+ pending: number;
575
+ percent: number;
576
+ targetCount: number;
529
577
  };
530
578
  diagnostic: {
531
579
  totalRows: number;
@@ -103,6 +103,11 @@ function resolveMaxProspects(value) {
103
103
  const parsed = typeof value === "number" && !Number.isNaN(value) ? value : fallback;
104
104
  return Math.min(MAX_PROSPECTS_LIMIT, Math.max(1, Math.floor(parsed)));
105
105
  }
106
+ function resolveMinPassedCount(value) {
107
+ if (typeof value !== "number" || Number.isNaN(value))
108
+ return null;
109
+ return Math.min(MAX_PROSPECTS_LIMIT, Math.max(1, Math.floor(value)));
110
+ }
106
111
  function mergeRubricsWithExisting(draftRubrics, existingRubrics) {
107
112
  const existingByCheckName = new Map();
108
113
  existingRubrics.forEach((item) => {
@@ -348,6 +353,10 @@ export const rubricToolDefinitions = [
348
353
  type: "number",
349
354
  description: `Number of completed rubric results to wait for (default ${DEFAULT_MAX_PROSPECTS}).`,
350
355
  },
356
+ minPassedCount: {
357
+ type: "number",
358
+ description: "Optional pass floor for bounded create-campaign samples. When this floor is met and remaining target rows are resolved as failed/not processing, the tool returns ready:true with partial:true instead of forcing a timeout.",
359
+ },
351
360
  timeoutMs: {
352
361
  type: "number",
353
362
  description: `Max time to wait in ms (default ${DEFAULT_TIMEOUT_MS}).`,
@@ -625,6 +634,7 @@ export async function waitForRubricResults(input) {
625
634
  const timeoutMs = Math.max(5000, input.timeoutMs ?? DEFAULT_TIMEOUT_MS);
626
635
  const intervalMs = Math.max(500, input.intervalMs ?? DEFAULT_INTERVAL_MS);
627
636
  const targetCount = resolveMaxProspects(input.targetCount);
637
+ const minPassedCount = resolveMinPassedCount(input.minPassedCount);
628
638
  const includeRows = input.includeRows !== false;
629
639
  let tableId = input.tableId;
630
640
  if (!tableId && input.campaignOfferId) {
@@ -642,10 +652,18 @@ export async function waitForRubricResults(input) {
642
652
  const stats = await api.get(`/api/v3/workflow-tables/${tableId}/stats`);
643
653
  lastStats = stats;
644
654
  const completed = stats.passRate?.completed ?? 0;
655
+ const passed = stats.passRate?.passed ?? 0;
645
656
  const totalRows = stats.totalRows ?? 0;
646
657
  const effectiveTarget = totalRows > 0 ? Math.min(targetCount, totalRows) : targetCount;
658
+ const pending = Math.max(effectiveTarget - completed, 0);
659
+ const failedCount = stats.failedCount ?? 0;
660
+ const processingCount = stats.processingCount ?? 0;
661
+ const queuedCount = stats.queuedCount ?? 0;
662
+ const cancellableCount = stats.cancellableCount ?? 0;
663
+ const minPassFloorMet = minPassedCount !== null && passed >= minPassedCount;
664
+ const unresolvedRowsResolvedAsFailures = pending > 0 && completed + failedCount >= effectiveTarget;
665
+ const noActiveProcessing = processingCount === 0 && queuedCount === 0 && cancellableCount === 0;
647
666
  if (completed >= effectiveTarget) {
648
- const passed = stats.passRate?.passed ?? 0;
649
667
  const percent = completed > 0 ? Math.round((passed / completed) * 100) : 0;
650
668
  const rowSnapshot = includeRows
651
669
  ? await getTableRowsMinimal(tableId, {
@@ -662,6 +680,46 @@ export async function waitForRubricResults(input) {
662
680
  passed,
663
681
  percent,
664
682
  targetCount: effectiveTarget,
683
+ ...(minPassedCount !== null ? { minPassedCount } : {}),
684
+ },
685
+ ...(rowSnapshot ? { rows: rowSnapshot.rows } : {}),
686
+ stats,
687
+ };
688
+ }
689
+ if (minPassFloorMet && noActiveProcessing) {
690
+ const percent = completed > 0 ? Math.round((passed / completed) * 100) : 0;
691
+ const reason = unresolvedRowsResolvedAsFailures
692
+ ? "min_passed_count_met_with_resolved_failures"
693
+ : "min_passed_count_met_no_active_processing";
694
+ const rowSnapshot = includeRows
695
+ ? await getTableRowsMinimal(tableId, {
696
+ limit: effectiveTarget,
697
+ })
698
+ : null;
699
+ return {
700
+ ready: true,
701
+ partial: true,
702
+ reason,
703
+ attempts,
704
+ elapsedMs: Date.now() - start,
705
+ tableId,
706
+ passRate: {
707
+ completed,
708
+ passed,
709
+ pending,
710
+ percent,
711
+ targetCount: effectiveTarget,
712
+ minPassedCount,
713
+ },
714
+ partialResult: {
715
+ completed,
716
+ passed,
717
+ pending,
718
+ percent,
719
+ targetCount: effectiveTarget,
720
+ minPassedCount,
721
+ enoughToDiagnose: true,
722
+ floorMet: true,
665
723
  },
666
724
  ...(rowSnapshot ? { rows: rowSnapshot.rows } : {}),
667
725
  stats,
@@ -687,6 +745,7 @@ export async function waitForRubricResults(input) {
687
745
  pending,
688
746
  percent,
689
747
  targetCount: effectiveTarget,
748
+ ...(minPassedCount !== null ? { minPassedCount } : {}),
690
749
  },
691
750
  partialResult: {
692
751
  completed,
@@ -694,7 +753,9 @@ export async function waitForRubricResults(input) {
694
753
  pending,
695
754
  percent,
696
755
  targetCount: effectiveTarget,
756
+ ...(minPassedCount !== null ? { minPassedCount } : {}),
697
757
  enoughToDiagnose: completed > 0,
758
+ floorMet: minPassedCount !== null && passed >= minPassedCount,
698
759
  },
699
760
  diagnostic: {
700
761
  totalRows,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.105",
3
+ "version": "0.1.107",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -1943,17 +1943,23 @@
1943
1943
  {
1944
1944
  "tool": "wait_for_rubric_results",
1945
1945
  "requiredFields": [
1946
- "targetCount"
1946
+ "targetCount",
1947
+ "minPassedCount"
1947
1948
  ],
1948
1949
  "targetCountSource": "stats.totalRows_or_imported_batch_count",
1950
+ "minPassedCountSource": "sample.minProjectedPass (3)",
1949
1951
  "requiredValues": {
1950
1952
  "includeRows": false
1951
1953
  },
1952
- "note": "The shell-first flow tests 15 leads first; always pass cohortSize explicitly instead of relying on default 25 behavior.",
1954
+ "note": "The shell-first flow tests 15 leads first; always pass cohortSize explicitly instead of relying on default 25 behavior. Pass minPassedCount so a 15-row batch with resolved failures can advance when the 3-pass floor is already met.",
1953
1955
  "readVia": "stats_only_tool_result",
1954
1956
  "extractFields": [
1955
1957
  "ready",
1958
+ "partial",
1959
+ "reason",
1956
1960
  "passRate.completed",
1961
+ "passRate.passed",
1962
+ "passRate.minPassedCount",
1957
1963
  "stats"
1958
1964
  ],
1959
1965
  "doNotRetain": "rows_payload",
@@ -224,6 +224,17 @@ campaignOfferId, confirmed: true })` -> `search_signals({ campaignOfferId,
224
224
  selectionMode: "replace", selections, headlineICPCriteria })` ->
225
225
  `import_leads({ campaignOfferId, provider: "signal-discovery",
226
226
  targetLeadCount: importLimit })`.
227
+ For Signal Discovery, the promotion/select step is load-bearing. Use
228
+ post IDs from the current campaign-scoped `search_signals` response or
229
+ posts the user has visibly promoted in the campaign UI. Never use post IDs
230
+ copied only from a source-scout summary unless they have been re-resolved
231
+ through the current campaign search state. After `select_promising_posts`,
232
+ require `selectedCount > 0` before calling `import_leads`. If it returns
233
+ `selectedCount: 0`, do not switch providers and do not retry import.
234
+ Explain that the campaign has no promoted Signal Discovery posts yet,
235
+ re-run a narrow campaign-scoped `search_signals` call to recover current
236
+ post rows, or ask the user to promote the desired posts in the UI and then
237
+ retry `import_leads`.
227
238
  Source approval is the explicit confirmation for this bounded review
228
239
  batch; do not ask for a second yes/no gate between
229
240
  `select_promising_posts` and `import_leads`.
@@ -275,11 +286,14 @@ Shape:
275
286
  queue_cells({ tableId: workflowTableId, cellIds: reviewBatchEnrichCellIds })
276
287
  wait_for_campaign_table_ready({ tableId: workflowTableId })
277
288
  get_rows_minimal({ tableId: workflowTableId })
278
- wait_for_rubric_results({ tableId: workflowTableId, targetCount: cohortSize, includeRows: false })
289
+ wait_for_rubric_results({ tableId: workflowTableId, targetCount: cohortSize, minPassedCount: minProjectedPass, includeRows: false })
279
290
  passInSample = count of first sampleSize review-batch rows with passesRubric === true
280
291
  projectedPass = round(passInSample / sampleSize * importLimit)
281
292
 
282
- if wait_for_rubric_results.ready === false and reason === "timeout":
293
+ if wait_for_rubric_results.ready === true and partial === true and reason === "min_passed_count_met_with_resolved_failures":
294
+ use the partial passRate/stats as a valid sample diagnostic
295
+ continue if projectedPass >= minProjectedPass and generated messages are present for the passing rows
296
+ else if wait_for_rubric_results.ready === false and reason === "timeout":
283
297
  use the partial passRate/stats as the sample diagnostic
284
298
  if active processing is visible:
285
299
  wait one more time at most
@@ -377,6 +377,14 @@ selections, headlineICPCriteria })`, then call
377
377
  targetLeadCount })` for the approved review batch without asking for another
378
378
  yes/no gate. Do not import more than the approved bounded batch.
379
379
 
380
+ The promotion/select step is required campaign state. Use post IDs from the
381
+ current campaign-scoped search result, not stale IDs copied from a source review
382
+ or scout summary. If `select_promising_posts` returns `selectedCount: 0`, stop:
383
+ do not call `import_leads`, do not switch providers automatically, and do not
384
+ claim the source failed. Re-run a narrow campaign-scoped search to recover
385
+ current post rows, or have the user promote the posts in the UI and then retry
386
+ the bounded import.
387
+
380
388
  ```json
381
389
  import_leads({
382
390
  "campaignOfferId": "cmp_xxx",