@sellable/mcp 0.1.139 → 0.1.140

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
@@ -243,9 +243,6 @@ function checkMessages(campaign) {
243
243
  if (!hasApprovedMessageTemplate(campaign)) {
244
244
  missing.push("approvedMessageTemplate");
245
245
  }
246
- if (campaign.currentStep === "auto-execute-messaging") {
247
- missing.push("generatedMessageReview");
248
- }
249
246
  return { stepId: "messages", missing };
250
247
  }
251
248
  function checkSettings(campaign) {
@@ -58,6 +58,7 @@ export declare function upsertRubric(input: UpsertRubricInput): Promise<{
58
58
  export type CampaignRubricsResponse = {
59
59
  campaignOfferId: string;
60
60
  enableICPFilters: boolean | null;
61
+ currentStep?: string | null;
61
62
  workflowTableId: string | null;
62
63
  rubrics: RubricItem[];
63
64
  };
@@ -341,7 +341,7 @@ export function getPostFindLeadsScoutRegistry() {
341
341
  usage: {
342
342
  codex: "After the user approves or auto-confirms the lead source, spawn both returned scout `name` values in one assistant turn only when the current Codex host exposes those custom agents.",
343
343
  claude: "After lead source approval, invoke both returned Task/Agent subagents in one assistant message only when the current Claude session lists those agents, so filter-leads and message generation run concurrently.",
344
- parentThreadRule: "Named agents are optional acceleration. If they are absent, do not customer-surface install status; the main thread still orchestrates filter and message branches from CampaignOffer state, selected source state, workflowTableId, and imported review-batch rows. Local markdown/json files are not normal-path inputs. The message branch must load the full generate-messages prompt, must read live campaign/review-batch state through scoped MCP/product tools, must reject mismatched selectedLeadListId/workflowTableId/campaign/workspace input, and may use the create-campaign-v2 message-review safety gate as a supplemental approval check. Join before message review. Do not automatically rerun Message Draft Builder after filters/enrichment finish; show the initial draft by default and offer an enriched rewrite only with explicit user opt-in.",
344
+ parentThreadRule: "Named agents are optional acceleration. If they are absent, do not customer-surface install status; the main thread still orchestrates filter and message branches from CampaignOffer state, selected source state, workflowTableId, and imported review-batch rows. Local markdown/json files are not normal-path inputs. The message drafting branch must load the full generate-messages prompt, must read live campaign/review-batch state through scoped MCP/product tools, and must reject mismatched selectedLeadListId/workflowTableId/campaign/workspace input. Template approval in the parent thread uses the create-campaign-v2 message-review safety gate; do not load the full generate-messages prompt for approval-only review. Join before message review. Do not automatically rerun Message Draft Builder after filters/enrichment finish; show the initial draft by default and offer an enriched rewrite only with explicit user opt-in.",
345
345
  },
346
346
  };
347
347
  }
@@ -481,6 +481,8 @@ export declare function saveRubrics(input: SaveRubricsInput): Promise<{
481
481
  criteriaCount: number;
482
482
  deletedCount: number;
483
483
  enableICPFiltersSet: boolean;
484
+ currentStep: string | null;
485
+ currentStepSet: boolean;
484
486
  rubrics: RubricItem[];
485
487
  activeRubrics: RubricItem[];
486
488
  }>;
@@ -85,9 +85,39 @@ async function fetchCampaignOffer(campaignOfferId) {
85
85
  return {
86
86
  workflowTableId: v3.workflowTableId,
87
87
  enableICPFilters: v3.enableICPFilters,
88
+ currentStep: v3.currentStep,
88
89
  leadScoringRubrics: v3.rubrics,
89
90
  };
90
91
  }
92
+ const filterRulesEntryWatchNarration = {
93
+ stage: "fit-message",
94
+ headline: "Create filter rules",
95
+ visibleState: "Filters are enabled and the browser is showing Filter Rules while the rubric is saved.",
96
+ agentIntent: "Codex is defining fit rules before any enrichment, filtering, or message cells run.",
97
+ nextAction: "Review saved filter rules",
98
+ progressLabel: "Fit + message",
99
+ safety: "No enrichment, filtering, Generate Message cells, sequence setup, or sending starts from this step.",
100
+ };
101
+ function shouldMoveToFilterRules(currentStep) {
102
+ if (!currentStep)
103
+ return true;
104
+ return [
105
+ "confirm-lead-list",
106
+ "filter-choice",
107
+ "create-icp-rubric",
108
+ "filter-rules",
109
+ ].includes(currentStep);
110
+ }
111
+ function buildEnableIcpFiltersPayload(currentStep) {
112
+ const payload = {
113
+ enableICPFilters: true,
114
+ };
115
+ if (shouldMoveToFilterRules(currentStep)) {
116
+ payload.currentStep = "create-icp-rubric";
117
+ payload.watchNarration = filterRulesEntryWatchNarration;
118
+ }
119
+ return payload;
120
+ }
91
121
  async function getRubricsForEdit(campaignOfferId) {
92
122
  const draft = getDraftRubrics(campaignOfferId);
93
123
  if (draft) {
@@ -204,7 +234,7 @@ export const rubricToolDefinitions = [
204
234
  },
205
235
  {
206
236
  name: "save_rubrics",
207
- description: "Persist rubric criteria to the campaign. Pass leadScoringRubrics directly to save without drafting. Saving active rubrics enables ICP filtering on the campaign so the client can continue to Filter Leads.",
237
+ description: "Persist rubric criteria to the campaign. Pass leadScoringRubrics directly to save without drafting. Saving active rubrics enables ICP filtering and keeps/moves the watched client to Filter Rules; it does not apply Filter Leads by itself.",
208
238
  inputSchema: {
209
239
  type: "object",
210
240
  properties: {
@@ -534,26 +564,43 @@ export async function saveRubrics(input) {
534
564
  // column cells=false → ICP Score runCondition evaluated false → entire
535
565
  // cascade blocked.
536
566
  let enableICPFiltersSet = false;
567
+ let currentStepSet = false;
537
568
  try {
538
569
  const api = getApi();
539
- await api.put(`/api/v2/campaign-offers/${input.campaignOfferId}`, {
540
- enableICPFilters: true,
541
- });
570
+ const payload = buildEnableIcpFiltersPayload(campaign.currentStep);
571
+ await api.put(`/api/v2/campaign-offers/${input.campaignOfferId}`, payload);
542
572
  enableICPFiltersSet = true;
573
+ currentStepSet = payload.currentStep === "create-icp-rubric";
543
574
  }
544
575
  catch (error) {
545
- // Non-fatal: the rubric save already succeeded. Log and
546
- // let the caller fix the flag via update_campaign if needed.
547
- console.error("[save_rubrics] Could not auto-enable ICP filtering on campaign", input.campaignOfferId, error);
576
+ // Non-fatal: the rubric save already succeeded. Retry the minimum
577
+ // enablement payload in case a stale step beat was rejected.
578
+ try {
579
+ const api = getApi();
580
+ await api.put(`/api/v2/campaign-offers/${input.campaignOfferId}`, {
581
+ enableICPFilters: true,
582
+ });
583
+ enableICPFiltersSet = true;
584
+ console.warn("[save_rubrics] Enabled ICP filtering without moving currentStep", input.campaignOfferId, error);
585
+ }
586
+ catch (fallbackError) {
587
+ console.error("[save_rubrics] Could not auto-enable ICP filtering on campaign", input.campaignOfferId, fallbackError);
588
+ }
548
589
  }
549
590
  return {
550
591
  success: true,
551
- message: enableICPFiltersSet || campaign.enableICPFilters === true
552
- ? `Saved ${normalizedDraft.length} rubric criteria and ICP filtering is ON.`
553
- : `Saved ${normalizedDraft.length} rubric criteria to the campaign. WARNING: could not auto-enable ICP filtering — call update_campaign({ campaignId, enableICPFilters: true }) to activate rubric-based filtering.`,
592
+ message: currentStepSet
593
+ ? `Saved ${normalizedDraft.length} rubric criteria, ICP filtering is ON, and the campaign is on Filter Rules.`
594
+ : enableICPFiltersSet || campaign.enableICPFilters === true
595
+ ? `Saved ${normalizedDraft.length} rubric criteria and ICP filtering is ON.`
596
+ : `Saved ${normalizedDraft.length} rubric criteria to the campaign. WARNING: could not auto-enable ICP filtering — call update_campaign({ campaignId, enableICPFilters: true }) to activate rubric-based filtering.`,
554
597
  criteriaCount: normalizedDraft.length,
555
598
  deletedCount: deletedRubricIds.length,
556
599
  enableICPFiltersSet,
600
+ currentStep: currentStepSet
601
+ ? "create-icp-rubric"
602
+ : (campaign.currentStep ?? null),
603
+ currentStepSet,
557
604
  rubrics: result?.rubrics ?? normalizedDraft,
558
605
  activeRubrics: result?.rubrics ?? normalizedDraft,
559
606
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.139",
3
+ "version": "0.1.140",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -117,6 +117,25 @@ data, compare sources by source volume, sampled ICP fit, activity/warmth
117
117
  signals, cleanup risk, and confidence basis. If a user asks for a forecast,
118
118
  label it explicitly as not estimated from this run.
119
119
 
120
+ Before any provider prompt, search, source scout, or signal-discovery call,
121
+ show a short source-plan gate and ask for approval. The gate should say:
122
+
123
+ - given this campaign, the viable source options
124
+ - the recommended first lane
125
+ - why that lane fits the buyer, offer, and likely public activity
126
+ - what will be tested next
127
+ - the fallback lane if relevant posts or ICP engagement look thin
128
+ - that approval authorizes scouting/search only, not lead import or sending
129
+
130
+ If active prospects likely engage with relevant LinkedIn content, recommend
131
+ LinkedIn post engagement / Signal Discovery and name the post themes you will
132
+ look for. If the niche is too private, low-volume, or unlikely to have relevant
133
+ public posts, recommend Sales Nav recent activity, broader Sales Nav role/title
134
+ filters, or Prospeo, and explain the tradeoff. Do not call `search_signals`,
135
+ `search_sales_nav`, `search_prospeo`, `fetch_post_engagers`, or provider-scoped
136
+ subagents until the user approves this source plan or explicitly chooses a
137
+ different source.
138
+
120
139
  When the user has not supplied a source and multiple source angles are viable,
121
140
  scout those angles as independent branches when the host can actually do it:
122
141
  LinkedIn Engagement / active post engagers (internal `signal-discovery`
@@ -577,9 +596,12 @@ updates.
577
596
  queue workflow cells, attach a sequence, or start until the filter choice is
578
597
  resolved, rubrics are saved when filters are enabled, template/token rules
579
598
  are approved on the default Use Template path, and the approved message set
580
- is synced into the campaign brief. After rubrics save, pause and say the fit
581
- rules are saved; approve the message template next; after approval, queue the
582
- bounded review-batch `enrichCellId` cells to kick off enrichment/filtering.
599
+ is synced into the campaign brief. When filters are approved, immediately
600
+ call `mcp__sellable__update_campaign({ campaignId, enableICPFilters: true, currentStep: "create-icp-rubric", watchNarration })`
601
+ so the watched app moves to Filter Rules while rubrics are drafted/saved.
602
+ After rubrics save, pause and say the fit rules are saved; approve the
603
+ message template next; after approval, queue the bounded review-batch
604
+ `enrichCellId` cells to kick off enrichment/filtering.
583
605
  Product Generate Message cells must not run from the background template
584
606
  path before that template/token approval.
585
607
  Do not ask the user to approve the brief before shell creation unless they
@@ -588,8 +610,9 @@ updates.
588
610
  `mcp__sellable__update_campaign({ campaignId, currentStep })` before major
589
611
  visible work so the user can watch progress in the app: `create-offer` for
590
612
  the brief, `pick-provider` or the selected provider step while sourcing,
591
- `filter-choice` after the 15-row review batch, `messages` for the Use
592
- Template / AI Generated mode choice, `auto-execute-messaging` for approved
613
+ `filter-choice` after the 15-row review batch, `create-icp-rubric` as soon
614
+ as filters are approved, `messages` for the Use Template / AI Generated mode
615
+ choice, `auto-execute-messaging` for approved
593
616
  message work or the product's AI-generated path, `awaiting-user-greenlight`
594
617
  for the final handoff, `settings` for sender selection, `sequence` after
595
618
  sender attach, and `send` once the recommended sequence is attached.
@@ -122,6 +122,26 @@ Default source order when the user has not supplied a source:
122
122
  3. broader Sales Nav role/title filters
123
123
  4. Prospeo account/contact expansion
124
124
 
125
+ Before any provider prompt, search, source scout, or signal-discovery call,
126
+ show a short source-plan gate and ask for approval. The gate must say, in plain
127
+ language:
128
+
129
+ - given this campaign, the viable source options
130
+ - the recommended first lane
131
+ - why that lane fits the buyer, offer, and likely public activity
132
+ - what will be tested next
133
+ - the fallback lane if relevant posts or ICP engagement look thin
134
+ - that approval authorizes scouting/search only, not lead import or sending
135
+
136
+ If active prospects likely engage with relevant LinkedIn content, recommend
137
+ LinkedIn post engagement / Signal Discovery and name the post themes you will
138
+ look for. If the niche is too private, low-volume, or unlikely to have relevant
139
+ public posts, recommend Sales Nav recent activity, broader Sales Nav role/title
140
+ filters, or Prospeo, and explain the tradeoff. Do not call `search_signals`,
141
+ `search_sales_nav`, `search_prospeo`, `fetch_post_engagers`, or provider-scoped
142
+ subagents until the user approves this source plan or explicitly chooses a
143
+ different source.
144
+
125
145
  Call `get_source_scout_registry` before source scouting. Source scouting is
126
146
  sequential by default. Run `source-scout-linkedin-engagement`,
127
147
  `source-scout-sales-nav`, and `source-scout-prospeo-contact` in parallel only
@@ -145,6 +165,11 @@ After `confirm_lead_list` imports a non-empty bounded review batch and
145
165
  CampaignOffer/source/table state as the source of truth. Debug markdown/json
146
166
  artifacts are optional only.
147
167
 
168
+ When the user chooses filters, immediately call
169
+ `update_campaign({ campaignId, enableICPFilters: true, currentStep: "create-icp-rubric", watchNarration })`
170
+ before rubric thinking or branch work. The watched app should move to Filter
171
+ Rules quickly, then show the saved rubric there after `save_rubrics`.
172
+
148
173
  Lead Fit Builder persists production rubrics with `save_rubrics` when filters
149
174
  are enabled. It must not require `brief.md`, `lead-review.md`, or
150
175
  `lead-sample.json`.
@@ -228,6 +228,14 @@ setting: `~/.codex/config.toml` with
228
228
 
229
229
  ## Approval Ethic
230
230
 
231
+ Lead sourcing has two approvals. First, before any provider prompt/search/scout
232
+ call, give the user a compact source plan: "given this campaign, these are the
233
+ real options, I recommend this first lane, here is why, here is the fallback if
234
+ the evidence is thin." Ask them to approve that plan or choose a different
235
+ source. That approval only authorizes scouting/search. Second, after the
236
+ source evidence exists, show counts, samples, fit math, cleanup risk, and ask
237
+ for source approval before import.
238
+
231
239
  ## Parallelism + Naming
232
240
 
233
241
  Source selection is sequential by default. Start with the first recommended
@@ -283,6 +283,27 @@
283
283
  "id": "find-leads",
284
284
  "label": "Find leads",
285
285
  "sourceSelectionFunnel": {
286
+ "preScoutRecommendationGate": {
287
+ "required": true,
288
+ "label": "Source plan approval",
289
+ "mustHappenBefore": [
290
+ "get_provider_prompt",
291
+ "search_signals",
292
+ "search_sales_nav",
293
+ "search_prospeo",
294
+ "fetch_post_engagers",
295
+ "source-scout dispatch"
296
+ ],
297
+ "show": [
298
+ "campaign-specific options considered",
299
+ "recommended first lane",
300
+ "why this lane matches the offer and ICP",
301
+ "expected evidence to gather",
302
+ "fallback lane if the first lane is weak",
303
+ "what approval authorizes"
304
+ ],
305
+ "approvalAuthorizes": "source scouting/search only; no lead import, review-batch creation, filtering, messaging, sequence, or launch"
306
+ },
286
307
  "defaultWhenSourceUnspecified": [
287
308
  "signal-discovery",
288
309
  "sales-nav-recent-active",
@@ -306,16 +327,47 @@
306
327
  "tool": "update_campaign",
307
328
  "requiredValues": {
308
329
  "currentStep": "pick-provider",
309
- "watchNarration.stage": "find-leads"
330
+ "watchNarration.stage": "find-leads",
331
+ "watchNarration.headline": "Choose the first source",
332
+ "watchNarration.visibleState": "The browser is showing source selection.",
333
+ "watchNarration.agentIntent": "Codex is explaining the source lane before sampling leads.",
334
+ "watchNarration.nextAction": "Approve a source lane",
335
+ "watchNarration.safety": "No leads import until a source is approved."
310
336
  },
311
- "purpose": "show the visible source-selection checkpoint before provider lanes"
337
+ "purpose": "show the visible source-plan approval checkpoint before provider lanes"
312
338
  },
313
339
  {
314
340
  "tool": "get_source_scout_registry",
315
341
  "purpose": "load canonical source scout names before optional branch launch"
316
342
  },
343
+ {
344
+ "action": "show_pre_scout_source_recommendation",
345
+ "uses": "request_user_input",
346
+ "requiredBeforeTools": [
347
+ "get_provider_prompt",
348
+ "search_signals",
349
+ "search_sales_nav",
350
+ "search_prospeo",
351
+ "fetch_post_engagers"
352
+ ],
353
+ "requiredInlineFields": [
354
+ "plain-language source options for this campaign",
355
+ "recommended first source lane",
356
+ "why the lane is likely to have relevant active prospects",
357
+ "why a database/recent-activity fallback may be better if niche conversations are thin",
358
+ "what scouting will check next",
359
+ "what approval authorizes"
360
+ ],
361
+ "choices": [
362
+ "Approve recommended source",
363
+ "Choose different source",
364
+ "Pause here"
365
+ ],
366
+ "approvalState": "source_lane_approved"
367
+ },
317
368
  {
318
369
  "action": "run_sequential_source_funnel",
370
+ "requiredPrecondition": "source_lane_approved",
319
371
  "defaultOrder": [
320
372
  "source-scout-linkedin-engagement",
321
373
  "source-scout-sales-nav",
@@ -532,7 +584,7 @@
532
584
  "tool": "update_campaign",
533
585
  "requiredValues": {
534
586
  "currentStep": "filter-choice",
535
- "watchNarration.stage": "filter-choice"
587
+ "watchNarration.stage": "fit-message"
536
588
  }
537
589
  },
538
590
  {
@@ -581,6 +633,30 @@
581
633
  "id": "post-lead-workstreams",
582
634
  "label": "Filter and message workstreams",
583
635
  "onEnter": [
636
+ {
637
+ "action": "persist_add_filters_approval",
638
+ "tool": "update_campaign",
639
+ "when": "filters_enabled",
640
+ "requiredFields": [
641
+ "campaignId",
642
+ "enableICPFilters",
643
+ "currentStep",
644
+ "watchNarration"
645
+ ],
646
+ "requiredValues": {
647
+ "enableICPFilters": true,
648
+ "currentStep": "create-icp-rubric",
649
+ "watchNarration.stage": "fit-message",
650
+ "watchNarration.headline": "Create filter rules",
651
+ "watchNarration.visibleState": "Filters are enabled and the browser is showing Filter Rules while Codex defines the rubric.",
652
+ "watchNarration.nextAction": "Review saved filter rules"
653
+ },
654
+ "mustRunBefore": [
655
+ "get_post_find_leads_scout_registry",
656
+ "launch_or_reuse_post_lead_workstreams",
657
+ "save_rubrics"
658
+ ]
659
+ },
584
660
  {
585
661
  "tool": "get_post_find_leads_scout_registry",
586
662
  "purpose": "load canonical post-lead worker names"
@@ -716,7 +792,7 @@
716
792
  "target": "post-find-leads-message-scout",
717
793
  "toolCallRequiredBeforeDraft": [
718
794
  "get_subskill_prompt({ subskillName: \"generate-messages\", offset, limit }) until hasMore=false",
719
- "optional supplemental check: get_subskill_asset({ subskillName: \"create-campaign-v2\", assetPath: \"references/message-review-safety-gate.md\" })"
795
+ "for approval-only parent-thread review, use get_subskill_asset({ subskillName: \"create-campaign-v2\", assetPath: \"references/message-review-safety-gate.md\" }) instead of loading generate-messages"
720
796
  ],
721
797
  "stateSource": "campaignBrief, selected source state, selectedLeadListId, workflowTableId, bounded review-batch row ids/hash",
722
798
  "outputState": "messageDraftRecommendation"
@@ -737,7 +813,7 @@
737
813
  "request_user_input"
738
814
  ],
739
815
  "toolRules": [
740
- "Before message review, load the full get_subskill_prompt({ subskillName: \"generate-messages\" }) prompt and draft from campaignBrief, selected source state, selectedLeadListId, workflowTableId, and imported review-batch rows.",
816
+ "Before message drafting, the Message Draft Builder branch loads the full get_subskill_prompt({ subskillName: \"generate-messages\" }) prompt and drafts from campaignBrief, selected source state, selectedLeadListId, workflowTableId, and imported review-batch rows. For approval-only parent-thread template review, load references/message-review-safety-gate.md and do not load the full generate-messages prompt.",
741
817
  "Do not use brief.md, lead-review.md, or lead-sample.json as required live state; those files are optional debug context only.",
742
818
  "The compact messageDraftRecommendation must return templateRecommendation, tokenFillRules, renderedSample, concerns, status, basisToken, outputAt, outputHash, and error or retry detail.",
743
819
  "If campaign/source/table/review-batch basis does not match, classify the output stale or blocked."
@@ -838,14 +914,15 @@
838
914
  "revise_messaging"
839
915
  ],
840
916
  "transitions": {
841
- "message_approved": "validate-sample",
917
+ "message_approved": "auto-execute-messaging",
842
918
  "revise_messaging": "message-generation"
843
919
  }
844
920
  },
845
921
  {
846
922
  "id": "validate-sample",
847
- "label": "Validate bounded review batch",
923
+ "label": "Validate bounded review batch (legacy)",
848
924
  "currentStepValue": "validate-sample",
925
+ "legacyOnly": true,
849
926
  "reference": "references/sample-validation-loop.md",
850
927
  "onEnter": [
851
928
  {
@@ -251,8 +251,8 @@ Messages waiting for template:
251
251
  {
252
252
  "stage": "review-ready",
253
253
  "headline": "Waiting for the template",
254
- "visibleState": "The fit rules are saved and the Message Draft Builder is still finishing.",
255
- "agentIntent": "Codex is waiting for the Message Draft Builder and will not ask for approval until the template can be reviewed.",
254
+ "visibleState": "The fit rules are saved and the message template is still being prepared.",
255
+ "agentIntent": "Codex is waiting until the template can be reviewed.",
256
256
  "nextAction": "Review template",
257
257
  "safety": "No enrichment, filtering, or Generate Message cells run until the template is approved."
258
258
  }
@@ -129,6 +129,12 @@ The kickoff doc is the resume surface. Re-open it before repeating discovery wor
129
129
  - Default source order for reply-likelihood-first outbound is `Signals -> Sales Nav -> Prospeo` unless the ask explicitly points elsewhere.
130
130
  - If the user is explicit about the goal, route from that goal first.
131
131
  - If the user is not explicit, infer the first hypothesis from the brief, then validate it with sample probes before recommending a lane.
132
+ - Before the first provider prompt, search, source scout, or signal-discovery
133
+ call, show the user a compact source plan and get approval. The plan must say
134
+ which campaign-specific options are plausible, which lane you recommend
135
+ first, why active LinkedIn engagers are or are not likely to exist for this
136
+ ICP/offer, what fallback lane you will use if the evidence is thin, and that
137
+ approval authorizes scouting/search only.
132
138
  - When enough context exists, try 1-2 alternate hypotheses if the first lane is too weak or noisy.
133
139
  - Directional preview does not require a sender, campaign, or selected lead list. Start with count/sample exploration first; only attach searches to a campaign when the user is ready to import.
134
140
  - If the user already has a LinkedIn-profile CSV, treat that as a direct lead-list path and skip discovery.