@sellable/mcp 0.1.804 → 0.1.805

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.
@@ -10,7 +10,7 @@ export declare const SELLABLE_MCP_TOOL_NAMES: readonly ["add_column", "add_on_de
10
10
  * `compileAgentToolPolicy`, so editing it cannot move the fleet-pinned
11
11
  * `policyHash`.
12
12
  */
13
- export declare const MUTATING_TOOL_NAMES: readonly ["pause_campaign", "archive_campaign", "update_campaign", "fill_campaign_horizon", "set_campaign_waterfall_order", "set_sender_routing", "send_inbox_draft", "send_inbox_manual_reply", "load_csv_dnc_entries", "update_cell", "queue_cells", "refill_v3_fill_ready", "refill_v3_edit_selected_campaign", "refill_v3_advance", "refill_v3_continue"];
13
+ export declare const MUTATING_TOOL_NAMES: readonly ["pause_campaign", "archive_campaign", "update_campaign", "fill_campaign_horizon", "set_campaign_waterfall_order", "set_sender_routing", "send_inbox_draft", "send_inbox_manual_reply", "load_csv_dnc_entries", "update_cell", "queue_cells", "check_rubric", "queue_campaign_cells", "start_campaign_message_preparation", "refill_v3_fill_ready", "refill_v3_edit_selected_campaign", "refill_v3_advance", "refill_v3_continue"];
14
14
  export declare function isMutatingTool(name: string): boolean;
15
15
  export declare const CROSS_WORKSPACE_EXPLICIT_WORKSPACE_REQUIRED: {
16
16
  readonly code: "cross_workspace_explicit_workspace_required";
@@ -207,6 +207,11 @@ export const MUTATING_TOOL_NAMES = [
207
207
  "load_csv_dnc_entries",
208
208
  "update_cell",
209
209
  "queue_cells",
210
+ // Campaign-maintenance compatibility mutations share the semantic campaign
211
+ // processing owner, but still require an explicit cross-workspace target.
212
+ "check_rubric",
213
+ "queue_campaign_cells",
214
+ "start_campaign_message_preparation",
210
215
  // Phase 146.5 Plan 08: the bounded Refill V3 mutation. Its sibling
211
216
  // `refill_v3_world_state` is an OBSERVATION and deliberately stays out.
212
217
  "refill_v3_fill_ready",
@@ -44,7 +44,7 @@ function sanitizePreparationResponse(value) {
44
44
  export const campaignMessagePreparationToolDefinitions = [
45
45
  {
46
46
  name: "start_campaign_message_preparation",
47
- description: 'Start a bounded message-preparation job for a specific existing CampaignOffer campaignId/tableId. This is the active_campaigns existing-row path after resolve_campaign_fill_route, exact target re-read, and active prep-job check; it is not campaign creation and not evergreen horizon fill. It never launches the campaign, sends messages, or directly writes scheduledFor. The job processes rows top-down by WorkflowTableRow.position, queues pending Enrich Prospect cells first, lets ICP/rubric and Generate Message cascade, then marks ready or approves only the bounded cohort. Use this or rowSelector:{type:"needsEnrichment"} for regular refill frontiers; needsGeneratedMessage is only for already-passed generated-message repair and can skip earlier unenriched rows. Prepared/approved/ready rows are intermediate only; scheduled completion requires a later re-read proving scheduler-owned scheduled cells with non-null scheduledFor. Surface active preparation jobs, exhausted source rows, disconnected Sales Nav/deleted sender accounts, missing sequence state, and other sender-health blockers separately from prepared/approved/scheduled counts. Omit maxRowsToCheck and batchSize for the adaptive default: calibrate on at least 100 actually-enriched rows, estimate the row budget from observed rubric/pass yield, cap rows at 300, and process at most 100 newly checked rows at a time. The worker will not pull another row batch while the current checked batch still has queueable or active cells. Do not interpret checkedRows as enriched rows; use progress.enrichedRows, needsEnrichRows, activeCellCount, preparedMessages, approvedMessages, and stopReason.',
47
+ description: "Start a bounded message-preparation job for a specific existing CampaignOffer campaignId/tableId. This is a shared execution primitive used by campaign setup and the fenced Refill V3 coordinator; for fill/refill-sends intent, call refill_v3_advance instead of manually reconstructing the loop. Compatibility/recovery callers may use it only after resolve_campaign_fill_route, an exact target re-read, and an active prep-job check. It is not campaign creation or evergreen horizon fill, and it never launches the campaign, sends messages, directly writes scheduledFor, or rewrites scheduled/attempted outreach. The job processes eligible rows top-down by WorkflowTableRow.position, queues pending Enrich Prospect cells first, lets ICP/rubric and Generate Message cascade, then marks ready or approves only the bounded cohort. needsGeneratedMessage is only for already-passed generated-message repair and can skip earlier unenriched rows. Prepared/approved/ready rows are intermediate only; scheduled completion requires a later re-read proving scheduler-owned scheduled cells with non-null scheduledFor. Surface active preparation jobs, exhausted source rows, disconnected Sales Nav/deleted sender accounts, missing sequence state, and other sender-health blockers separately from prepared/approved/scheduled counts. Omit maxRowsToCheck and batchSize for the adaptive default: calibrate on at least 100 actually-enriched rows, estimate the row budget from observed rubric/pass yield, cap rows at 300, and process at most 100 newly checked rows at a time. The worker will not pull another row batch while the current checked batch still has queueable or active cells. Do not interpret checkedRows as enriched rows; use progress.enrichedRows, needsEnrichRows, activeCellCount, preparedMessages, approvedMessages, and stopReason. Cross-workspace Agent calls must pass workspaceId.",
48
48
  inputSchema: {
49
49
  type: "object",
50
50
  properties: {
@@ -419,7 +419,7 @@ export const campaignProcessingToolDefinitions = [
419
419
  },
420
420
  {
421
421
  name: "queue_campaign_cells",
422
- description: 'Resolve and queue campaign cells by semantic role and row selector. Normal create-campaign tail should use this instead of fetching rows only to discover cell IDs. For regular refill enrichment, queue columnRole:"enrich" with rowSelector:{type:"needsEnrichment"} or use start_campaign_message_preparation for the full top-down prep loop. For approval, rowSelector:{type:"needsApproval"} selects current generated rows missing Approved=true, but mutating Approved cells still requires explicit bounded approval. Do not use needsGeneratedMessage as a refill cursor because it only selects already-passed rows. forceRerun retries the selected cells only; it never changes the approved template or synthesizes messageTemplateRevision authority.',
422
+ description: 'Resolve and queue campaign cells by semantic role and bounded row selector. This is the shared campaign-setup, rubric-maintenance, and recovery owner; use it instead of fetching rows only to discover raw cell IDs. For any fill/refill-sends intent, do not call this tool directly: use refill_v3_advance so V3 owns target selection, fencing, and continuation. For approval, rowSelector:{type:"needsApproval"} selects current generated rows missing Approved=true, but mutating Approved cells still requires explicit bounded approval. Do not use needsGeneratedMessage as a top-down cursor because it only selects already-passed rows. forceRerun retries the selected cells only; it never changes the approved template, rewrites scheduled/attempted outreach, or synthesizes messageTemplateRevision authority. Cross-workspace Agent calls must pass workspaceId.',
423
423
  inputSchema: {
424
424
  type: "object",
425
425
  properties: {
@@ -24,7 +24,7 @@ export const cellToolDefinitions = [
24
24
  },
25
25
  {
26
26
  name: "queue_cells",
27
- description: "Queue explicit cells for processing. In create-campaign-v2 Filter Leads, pass only initial campaign-table execution-slice enrichCellId values from get_rows_minimal; do not pass icpCellId values or full-table cells because the workflow cascade handles downstream ICP scoring.",
27
+ description: "Legacy exact-cell compatibility/recovery tool. Prefer queue_campaign_cells for campaign work because it resolves semantic column roles and bounded row selectors. For fill/refill intent, use refill_v3_advance instead of reconstructing the refill loop. If exact IDs are required for recovery, pass only the bounded cells already returned by a product owner; do not queue full-table or downstream ICP cells. Cross-workspace Agent calls must pass workspaceId.",
28
28
  inputSchema: {
29
29
  type: "object",
30
30
  properties: {
@@ -11106,6 +11106,7 @@ export declare const allTools: ({
11106
11106
  updates?: undefined;
11107
11107
  checkName?: undefined;
11108
11108
  maxProspects?: undefined;
11109
+ workspaceId?: undefined;
11109
11110
  tableId?: undefined;
11110
11111
  targetCount?: undefined;
11111
11112
  minPassedCount?: undefined;
@@ -11140,6 +11141,7 @@ export declare const allTools: ({
11140
11141
  updates?: undefined;
11141
11142
  checkName?: undefined;
11142
11143
  maxProspects?: undefined;
11144
+ workspaceId?: undefined;
11143
11145
  tableId?: undefined;
11144
11146
  targetCount?: undefined;
11145
11147
  minPassedCount?: undefined;
@@ -11203,6 +11205,7 @@ export declare const allTools: ({
11203
11205
  updates?: undefined;
11204
11206
  checkName?: undefined;
11205
11207
  maxProspects?: undefined;
11208
+ workspaceId?: undefined;
11206
11209
  tableId?: undefined;
11207
11210
  targetCount?: undefined;
11208
11211
  minPassedCount?: undefined;
@@ -11262,6 +11265,7 @@ export declare const allTools: ({
11262
11265
  updates?: undefined;
11263
11266
  checkName?: undefined;
11264
11267
  maxProspects?: undefined;
11268
+ workspaceId?: undefined;
11265
11269
  tableId?: undefined;
11266
11270
  targetCount?: undefined;
11267
11271
  minPassedCount?: undefined;
@@ -11323,6 +11327,7 @@ export declare const allTools: ({
11323
11327
  rubric?: undefined;
11324
11328
  checkName?: undefined;
11325
11329
  maxProspects?: undefined;
11330
+ workspaceId?: undefined;
11326
11331
  tableId?: undefined;
11327
11332
  targetCount?: undefined;
11328
11333
  minPassedCount?: undefined;
@@ -11354,6 +11359,7 @@ export declare const allTools: ({
11354
11359
  rubricId?: undefined;
11355
11360
  updates?: undefined;
11356
11361
  maxProspects?: undefined;
11362
+ workspaceId?: undefined;
11357
11363
  tableId?: undefined;
11358
11364
  targetCount?: undefined;
11359
11365
  minPassedCount?: undefined;
@@ -11379,6 +11385,10 @@ export declare const allTools: ({
11379
11385
  type: string;
11380
11386
  description: string;
11381
11387
  };
11388
+ workspaceId: {
11389
+ type: string;
11390
+ description: string;
11391
+ };
11382
11392
  leadScoringRubrics?: undefined;
11383
11393
  requiredCheckNames?: undefined;
11384
11394
  rubric?: undefined;
@@ -27,6 +27,7 @@ type DeleteRubricItemInput = {
27
27
  type CheckRubricInput = {
28
28
  campaignOfferId: string;
29
29
  maxProspects?: number;
30
+ workspaceId?: string;
30
31
  };
31
32
  type WaitForRubricResultsInput = {
32
33
  campaignOfferId?: string;
@@ -112,6 +113,7 @@ export declare const rubricToolDefinitions: ({
112
113
  updates?: undefined;
113
114
  checkName?: undefined;
114
115
  maxProspects?: undefined;
116
+ workspaceId?: undefined;
115
117
  tableId?: undefined;
116
118
  targetCount?: undefined;
117
119
  minPassedCount?: undefined;
@@ -146,6 +148,7 @@ export declare const rubricToolDefinitions: ({
146
148
  updates?: undefined;
147
149
  checkName?: undefined;
148
150
  maxProspects?: undefined;
151
+ workspaceId?: undefined;
149
152
  tableId?: undefined;
150
153
  targetCount?: undefined;
151
154
  minPassedCount?: undefined;
@@ -209,6 +212,7 @@ export declare const rubricToolDefinitions: ({
209
212
  updates?: undefined;
210
213
  checkName?: undefined;
211
214
  maxProspects?: undefined;
215
+ workspaceId?: undefined;
212
216
  tableId?: undefined;
213
217
  targetCount?: undefined;
214
218
  minPassedCount?: undefined;
@@ -268,6 +272,7 @@ export declare const rubricToolDefinitions: ({
268
272
  updates?: undefined;
269
273
  checkName?: undefined;
270
274
  maxProspects?: undefined;
275
+ workspaceId?: undefined;
271
276
  tableId?: undefined;
272
277
  targetCount?: undefined;
273
278
  minPassedCount?: undefined;
@@ -329,6 +334,7 @@ export declare const rubricToolDefinitions: ({
329
334
  rubric?: undefined;
330
335
  checkName?: undefined;
331
336
  maxProspects?: undefined;
337
+ workspaceId?: undefined;
332
338
  tableId?: undefined;
333
339
  targetCount?: undefined;
334
340
  minPassedCount?: undefined;
@@ -360,6 +366,7 @@ export declare const rubricToolDefinitions: ({
360
366
  rubricId?: undefined;
361
367
  updates?: undefined;
362
368
  maxProspects?: undefined;
369
+ workspaceId?: undefined;
363
370
  tableId?: undefined;
364
371
  targetCount?: undefined;
365
372
  minPassedCount?: undefined;
@@ -385,6 +392,10 @@ export declare const rubricToolDefinitions: ({
385
392
  type: string;
386
393
  description: string;
387
394
  };
395
+ workspaceId: {
396
+ type: string;
397
+ description: string;
398
+ };
388
399
  leadScoringRubrics?: undefined;
389
400
  requiredCheckNames?: undefined;
390
401
  rubric?: undefined;
@@ -447,6 +458,7 @@ export declare const rubricToolDefinitions: ({
447
458
  updates?: undefined;
448
459
  checkName?: undefined;
449
460
  maxProspects?: undefined;
461
+ workspaceId?: undefined;
450
462
  };
451
463
  required: never[];
452
464
  additionalProperties: boolean;
@@ -503,15 +515,6 @@ export declare function saveRubrics(input: SaveRubricsInput): Promise<{
503
515
  activeRubrics: RubricItem[];
504
516
  }>;
505
517
  export declare function checkRubric(input: CheckRubricInput): Promise<{
506
- success: boolean;
507
- message: string;
508
- processedRows: number;
509
- tableId: string;
510
- queuedCount?: undefined;
511
- dispatchedCount?: undefined;
512
- failedCount?: undefined;
513
- skippedCount?: undefined;
514
- } | {
515
518
  success: boolean;
516
519
  message: string;
517
520
  processedRows: number;
@@ -1,4 +1,5 @@
1
1
  import { getApi } from "../api.js";
2
+ import { queueCampaignCells } from "./campaign-processing.js";
2
3
  import { fetchCampaignRubrics, upsertRubricV3, } from "./processing.js";
3
4
  import { getTableRowsMinimal } from "./rows.js";
4
5
  const rubricDrafts = new Map();
@@ -362,7 +363,7 @@ export const rubricToolDefinitions = [
362
363
  },
363
364
  {
364
365
  name: "check_rubric",
365
- description: "Queue enrichment + scoring for a sample of prospects (default 7). Requires saved rubrics.",
366
+ description: "Legacy compatibility/recovery alias for a bounded saved-rubric sample. It delegates to queue_campaign_cells with the semantic Enrich Prospect role and never discovers raw cell IDs. Do not use it for fill/refill intent; use refill_v3_advance. Normal campaign setup and maintenance should call queue_campaign_cells directly. Cross-workspace Agent calls must pass workspaceId.",
366
367
  inputSchema: {
367
368
  type: "object",
368
369
  properties: {
@@ -374,6 +375,10 @@ export const rubricToolDefinitions = [
374
375
  type: "number",
375
376
  description: `Number of prospects to test (default ${DEFAULT_MAX_PROSPECTS}, max ${MAX_PROSPECTS_LIMIT}).`,
376
377
  },
378
+ workspaceId: {
379
+ type: "string",
380
+ description: "Explicit target workspace id.",
381
+ },
377
382
  },
378
383
  required: ["campaignOfferId"],
379
384
  additionalProperties: false,
@@ -628,75 +633,57 @@ export async function saveRubrics(input) {
628
633
  };
629
634
  }
630
635
  export async function checkRubric(input) {
631
- const maxProspects = resolveMaxProspects(input.maxProspects);
632
- const campaign = await fetchCampaignOffer(input.campaignOfferId);
633
- if (campaign.enableICPFilters !== true) {
634
- throw new Error("ICP filtering is not enabled. Save and apply rubrics before running check_rubric.");
635
- }
636
- const draft = getDraftRubrics(input.campaignOfferId);
637
- const rubrics = normalizeCampaignRubrics((draft || campaign.leadScoringRubrics || []).filter((r) => r.useCheckForScoring !== false));
638
- if (rubrics.length === 0) {
639
- throw new Error("No active rubric criteria found. Use draft_rubrics and save_rubrics first.");
640
- }
641
- const unsavedRubrics = rubrics.filter((r) => !r.id);
642
- if (unsavedRubrics.length > 0) {
643
- throw new Error(`${unsavedRubrics.length} rubric criteria are not saved to database. Call save_rubrics before check_rubric.`);
644
- }
645
- const workflowTableId = campaign.workflowTableId;
646
- if (!workflowTableId) {
647
- throw new Error("No workflow table found. Complete the Find Leads step first to create a workflow table with leads.");
648
- }
649
- const api = getApi();
650
- const tableResponse = await api.get(`/api/v3/campaigns/${input.campaignOfferId}/workflow-table`);
651
- const workflowTable = tableResponse.workflowTable;
652
- if (!workflowTable) {
653
- throw new Error("Workflow table not found for this campaign.");
654
- }
655
- const enrichColumn = workflowTable.columns?.find((col) => col.columnType === "enrich_person");
656
- if (!enrichColumn) {
657
- throw new Error("No Enrich Person column found in workflow table. The table may not be set up correctly.");
658
- }
659
- const rows = workflowTable.rows || [];
660
- const cellsToQueue = [];
661
- for (const row of rows) {
662
- if (cellsToQueue.length >= maxProspects)
663
- break;
664
- const enrichCell = row.cells?.find((cell) => cell.columnId === enrichColumn.id);
665
- if (!enrichCell)
666
- continue;
667
- const queueableStatuses = ["pending", "error", "dependency_blocked"];
668
- if (queueableStatuses.includes(enrichCell.status || "")) {
669
- cellsToQueue.push(enrichCell.id);
636
+ const workspaceId = typeof input.workspaceId === "string" && input.workspaceId.trim()
637
+ ? input.workspaceId.trim()
638
+ : undefined;
639
+ const execute = async () => {
640
+ const maxProspects = resolveMaxProspects(input.maxProspects);
641
+ const campaign = await fetchCampaignOffer(input.campaignOfferId);
642
+ if (campaign.enableICPFilters !== true) {
643
+ throw new Error("ICP filtering is not enabled. Save and apply rubrics before running check_rubric.");
670
644
  }
671
- }
672
- if (cellsToQueue.length === 0) {
645
+ const draft = getDraftRubrics(input.campaignOfferId);
646
+ const rubrics = normalizeCampaignRubrics((draft || campaign.leadScoringRubrics || []).filter((rubric) => rubric.useCheckForScoring !== false));
647
+ if (rubrics.length === 0) {
648
+ throw new Error("No active rubric criteria found. Use draft_rubrics and save_rubrics first.");
649
+ }
650
+ const unsavedRubrics = rubrics.filter((rubric) => !rubric.id);
651
+ if (unsavedRubrics.length > 0) {
652
+ throw new Error(`${unsavedRubrics.length} rubric criteria are not saved to database. Call save_rubrics before check_rubric.`);
653
+ }
654
+ const workflowTableId = campaign.workflowTableId;
655
+ if (!workflowTableId) {
656
+ throw new Error("No workflow table found. Complete the Find Leads step first to create a workflow table with leads.");
657
+ }
658
+ const result = (await queueCampaignCells({
659
+ campaignId: input.campaignOfferId,
660
+ tableId: workflowTableId,
661
+ ...(workspaceId ? { workspaceId } : {}),
662
+ columnRole: "enrich",
663
+ rowSelector: { type: "needsEnrichment", limit: maxProspects },
664
+ limit: maxProspects,
665
+ reason: "check_rubric compatibility sample",
666
+ }));
667
+ const queuedCount = result.queue?.queued ?? 0;
668
+ const failedCount = result.queue?.failed ?? 0;
669
+ const skippedCount = result.queue?.skipped ?? 0;
670
+ const dispatchedCount = result.queue?.dispatched ?? queuedCount;
673
671
  return {
674
672
  success: true,
675
- message: "No rows need processing. All enrich cells are either completed, cached, or currently processing.",
676
- processedRows: 0,
677
- tableId: workflowTable.id,
673
+ message: queuedCount === 0
674
+ ? "No rows need processing. All semantic Enrich Prospect cells are completed, cached, or currently processing."
675
+ : `Queued ${queuedCount} prospects for enrichment and scoring.${failedCount > 0 ? ` (${failedCount} failed to dispatch)` : ""}`,
676
+ processedRows: queuedCount,
677
+ queuedCount,
678
+ dispatchedCount,
679
+ failedCount,
680
+ skippedCount,
681
+ tableId: workflowTableId,
678
682
  };
679
- }
680
- // Use the same robust batch queue path as Launch "Enrich All":
681
- // atomically queue + dispatch with lock protection.
682
- const queueResult = await api.post(`/api/v3/workflow-tables/cells/queue-batch`, {
683
- tableId: workflowTable.id,
684
- cellIds: cellsToQueue,
685
- });
686
- const queuedCount = queueResult?.queued ?? 0;
687
- const failedCount = queueResult?.failed ?? 0;
688
- const skippedCount = queueResult?.skipped ?? 0;
689
- const dispatchedCount = queueResult?.dispatched ?? queuedCount;
690
- return {
691
- success: true,
692
- message: `Queued ${queuedCount} prospects for enrichment and scoring.${failedCount > 0 ? ` (${failedCount} failed to dispatch)` : ""}`,
693
- processedRows: queuedCount,
694
- queuedCount,
695
- dispatchedCount,
696
- failedCount,
697
- skippedCount,
698
- tableId: workflowTable.id,
699
683
  };
684
+ return workspaceId
685
+ ? getApi().withWorkspaceId(workspaceId, execute)
686
+ : execute();
700
687
  }
701
688
  export async function waitForRubricResults(input) {
702
689
  const api = getApi();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.804",
3
+ "version": "0.1.805",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code, Codex, and Hermes campaign workflows",
6
6
  "main": "dist/index.js",
@@ -53,12 +53,14 @@ Create and save a comprehensive lead scoring rubric that:
53
53
 
54
54
  ## Rubric Validation + Results
55
55
 
56
- - `mcp__sellable__get_rows_minimal` - Read initial campaign-table execution-slice cell IDs
57
- - `mcp__sellable__queue_cells` - Queue only bounded sample `enrichCellId` values
56
+ - `mcp__sellable__queue_campaign_cells` - Queue a bounded sample by semantic
57
+ `columnRole:"enrich"` and row selector; pass `workspaceId` in cross-workspace
58
+ Agent mode
58
59
  - `mcp__sellable__wait_for_rubric_results` - Poll pass-rate results
59
60
 
60
- `check_rubric` is legacy/recovery only. Do not use it in normal
61
- create-campaign-v2 runs.
61
+ `check_rubric` and `queue_cells` are legacy/recovery only. Do not use either in
62
+ normal create-campaign-v2 runs, and do not use any of these tools for refill
63
+ intent; Refill V3 owns refill execution.
62
64
 
63
65
  </tools>
64
66
 
@@ -304,11 +306,10 @@ rubric editing, ask the user if we should kick off enrichment for a sample:
304
306
 
305
307
  ## Phase 6: Enrich + Score a Sample
306
308
 
307
- 1. Call `get_rows_minimal({ tableId, limit: 5 })` or use the caller-provided
308
- review/process sample rows.
309
- 2. Call `queue_cells({ tableId, cellIds })` with only pending/error
310
- `enrichCellId` values. Do not pass `icpCellId` values or full-table cells;
311
- the workflow cascade handles downstream ICP scoring after enrichment.
309
+ 1. Use the caller-provided bounded review/process sample.
310
+ 2. Call `queue_campaign_cells({ campaignId, tableId, columnRole: "enrich",
311
+ rowSelector: { type: "reviewBatch" }, workspaceId })`. The semantic owner
312
+ resolves Enrich Prospect and preserves the workflow cascade into scoring.
312
313
  3. Call `wait_for_rubric_results` with `targetCount: 5` and `minPassedCount: 1`
313
314
  — polls until ICP scores land.
314
315
  4. Summarize pass rate (`passed/completed`, percent) and recommend: