@sellable/mcp 0.1.105 → 0.1.106

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;
@@ -109,6 +110,7 @@ export declare const rubricToolDefinitions: ({
109
110
  maxProspects?: undefined;
110
111
  tableId?: undefined;
111
112
  targetCount?: undefined;
113
+ minPassedCount?: undefined;
112
114
  timeoutMs?: undefined;
113
115
  intervalMs?: undefined;
114
116
  includeRows?: undefined;
@@ -141,6 +143,7 @@ export declare const rubricToolDefinitions: ({
141
143
  maxProspects?: undefined;
142
144
  tableId?: undefined;
143
145
  targetCount?: undefined;
146
+ minPassedCount?: undefined;
144
147
  timeoutMs?: undefined;
145
148
  intervalMs?: undefined;
146
149
  includeRows?: undefined;
@@ -198,6 +201,7 @@ export declare const rubricToolDefinitions: ({
198
201
  maxProspects?: undefined;
199
202
  tableId?: undefined;
200
203
  targetCount?: undefined;
204
+ minPassedCount?: undefined;
201
205
  timeoutMs?: undefined;
202
206
  intervalMs?: undefined;
203
207
  includeRows?: undefined;
@@ -255,6 +259,7 @@ export declare const rubricToolDefinitions: ({
255
259
  maxProspects?: undefined;
256
260
  tableId?: undefined;
257
261
  targetCount?: undefined;
262
+ minPassedCount?: undefined;
258
263
  timeoutMs?: undefined;
259
264
  intervalMs?: undefined;
260
265
  includeRows?: undefined;
@@ -314,6 +319,7 @@ export declare const rubricToolDefinitions: ({
314
319
  maxProspects?: undefined;
315
320
  tableId?: undefined;
316
321
  targetCount?: undefined;
322
+ minPassedCount?: undefined;
317
323
  timeoutMs?: undefined;
318
324
  intervalMs?: undefined;
319
325
  includeRows?: undefined;
@@ -343,6 +349,7 @@ export declare const rubricToolDefinitions: ({
343
349
  maxProspects?: undefined;
344
350
  tableId?: undefined;
345
351
  targetCount?: undefined;
352
+ minPassedCount?: undefined;
346
353
  timeoutMs?: undefined;
347
354
  intervalMs?: undefined;
348
355
  includeRows?: undefined;
@@ -372,6 +379,7 @@ export declare const rubricToolDefinitions: ({
372
379
  checkName?: undefined;
373
380
  tableId?: undefined;
374
381
  targetCount?: undefined;
382
+ minPassedCount?: undefined;
375
383
  timeoutMs?: undefined;
376
384
  intervalMs?: undefined;
377
385
  includeRows?: undefined;
@@ -397,6 +405,10 @@ export declare const rubricToolDefinitions: ({
397
405
  type: string;
398
406
  description: string;
399
407
  };
408
+ minPassedCount: {
409
+ type: string;
410
+ description: string;
411
+ };
400
412
  timeoutMs: {
401
413
  type: string;
402
414
  description: string;
@@ -496,6 +508,7 @@ export declare function waitForRubricResults(input: WaitForRubricResultsInput):
496
508
  elapsedMs: number;
497
509
  tableId: string;
498
510
  passRate: {
511
+ minPassedCount?: number | undefined;
499
512
  completed: number;
500
513
  passed: number;
501
514
  percent: number;
@@ -507,7 +520,10 @@ export declare function waitForRubricResults(input: WaitForRubricResultsInput):
507
520
  diagnostic?: undefined;
508
521
  guidance?: undefined;
509
522
  } | {
523
+ stats: WorkflowTableStats;
524
+ rows?: import("./rows.js").LightweightRow[] | undefined;
510
525
  ready: boolean;
526
+ partial: boolean;
511
527
  reason: string;
512
528
  attempts: number;
513
529
  elapsedMs: number;
@@ -518,6 +534,7 @@ export declare function waitForRubricResults(input: WaitForRubricResultsInput):
518
534
  pending: number;
519
535
  percent: number;
520
536
  targetCount: number;
537
+ minPassedCount: number;
521
538
  };
522
539
  partialResult: {
523
540
  completed: number;
@@ -525,7 +542,35 @@ export declare function waitForRubricResults(input: WaitForRubricResultsInput):
525
542
  pending: number;
526
543
  percent: number;
527
544
  targetCount: number;
545
+ minPassedCount: number;
546
+ enoughToDiagnose: boolean;
547
+ floorMet: boolean;
548
+ };
549
+ diagnostic?: undefined;
550
+ guidance?: undefined;
551
+ } | {
552
+ ready: boolean;
553
+ reason: string;
554
+ attempts: number;
555
+ elapsedMs: number;
556
+ tableId: string;
557
+ passRate: {
558
+ minPassedCount?: number | undefined;
559
+ completed: number;
560
+ passed: number;
561
+ pending: number;
562
+ percent: number;
563
+ targetCount: number;
564
+ };
565
+ partialResult: {
528
566
  enoughToDiagnose: boolean;
567
+ floorMet: boolean;
568
+ minPassedCount?: number | undefined;
569
+ completed: number;
570
+ passed: number;
571
+ pending: number;
572
+ percent: number;
573
+ targetCount: number;
529
574
  };
530
575
  diagnostic: {
531
576
  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,17 @@ 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 needsEnrichCount = stats.needsEnrichCount ?? 0;
662
+ const minPassFloorMet = minPassedCount !== null && passed >= minPassedCount;
663
+ const unresolvedRowsResolvedAsFailures = pending > 0 && completed + failedCount >= effectiveTarget;
664
+ const noActiveProcessing = processingCount === 0 && needsEnrichCount === 0;
647
665
  if (completed >= effectiveTarget) {
648
- const passed = stats.passRate?.passed ?? 0;
649
666
  const percent = completed > 0 ? Math.round((passed / completed) * 100) : 0;
650
667
  const rowSnapshot = includeRows
651
668
  ? await getTableRowsMinimal(tableId, {
@@ -662,6 +679,45 @@ export async function waitForRubricResults(input) {
662
679
  passed,
663
680
  percent,
664
681
  targetCount: effectiveTarget,
682
+ ...(minPassedCount !== null ? { minPassedCount } : {}),
683
+ },
684
+ ...(rowSnapshot ? { rows: rowSnapshot.rows } : {}),
685
+ stats,
686
+ };
687
+ }
688
+ if (minPassFloorMet &&
689
+ unresolvedRowsResolvedAsFailures &&
690
+ noActiveProcessing) {
691
+ const percent = completed > 0 ? Math.round((passed / completed) * 100) : 0;
692
+ const rowSnapshot = includeRows
693
+ ? await getTableRowsMinimal(tableId, {
694
+ limit: effectiveTarget,
695
+ })
696
+ : null;
697
+ return {
698
+ ready: true,
699
+ partial: true,
700
+ reason: "min_passed_count_met_with_resolved_failures",
701
+ attempts,
702
+ elapsedMs: Date.now() - start,
703
+ tableId,
704
+ passRate: {
705
+ completed,
706
+ passed,
707
+ pending,
708
+ percent,
709
+ targetCount: effectiveTarget,
710
+ minPassedCount,
711
+ },
712
+ partialResult: {
713
+ completed,
714
+ passed,
715
+ pending,
716
+ percent,
717
+ targetCount: effectiveTarget,
718
+ minPassedCount,
719
+ enoughToDiagnose: true,
720
+ floorMet: true,
665
721
  },
666
722
  ...(rowSnapshot ? { rows: rowSnapshot.rows } : {}),
667
723
  stats,
@@ -687,6 +743,7 @@ export async function waitForRubricResults(input) {
687
743
  pending,
688
744
  percent,
689
745
  targetCount: effectiveTarget,
746
+ ...(minPassedCount !== null ? { minPassedCount } : {}),
690
747
  },
691
748
  partialResult: {
692
749
  completed,
@@ -694,7 +751,9 @@ export async function waitForRubricResults(input) {
694
751
  pending,
695
752
  percent,
696
753
  targetCount: effectiveTarget,
754
+ ...(minPassedCount !== null ? { minPassedCount } : {}),
697
755
  enoughToDiagnose: completed > 0,
756
+ floorMet: minPassedCount !== null && passed >= minPassedCount,
698
757
  },
699
758
  diagnostic: {
700
759
  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.106",
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",