@sellable/mcp 0.1.141 → 0.1.143

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/README.md CHANGED
@@ -21,7 +21,9 @@ There are two public Sellable entrypoints shared across hosts:
21
21
  - `sellable:create-campaign`
22
22
  - `sellable:interview`
23
23
 
24
- The create-campaign public wrapper loads the approval-gated workflow from:
24
+ The create-campaign public wrapper at
25
+ `mcp/sellable/skills/create-campaign/SKILL.md` handles auth/bootstrap and loads
26
+ the approval-gated workflow from:
25
27
 
26
28
  - `mcp/sellable/skills/create-campaign-v2/SKILL.md`
27
29
 
@@ -30,9 +32,7 @@ from:
30
32
 
31
33
  - `mcp/sellable/skills/interview/SKILL.md`
32
34
 
33
- The older `mcp/sellable/skills/create-campaign/SKILL.md` prompt is kept as an
34
- internal legacy prompt for compatibility only. Do not advertise it as a public
35
- command.
35
+ Keep `create-campaign-v2` internal. Do not advertise it as a public command.
36
36
 
37
37
  ### 1. One-Command Agent Install
38
38
 
@@ -68,6 +68,7 @@ startup/auth update check and tells the agent to run
68
68
  Publish from the repo root with:
69
69
 
70
70
  ```bash
71
+ npm run validate:sellable-skills
71
72
  npm run mcp:publish
72
73
  ```
73
74
 
@@ -61,9 +61,9 @@ Return the following to the parent thread:
61
61
  choice, and rubric/filter basis when present
62
62
  - output timestamp/hash and any retry/error detail
63
63
 
64
- Write `message-validation.md`, `message-prep.md`, or
65
- `message-candidate-drafts.md` only when the parent explicitly asks for debug
66
- artifacts. Normal live campaign runs can return the same content directly.
64
+ Do not write local markdown/json artifacts in normal live campaign runs. Return
65
+ the recommendation directly to the parent thread. Emit debug artifacts only when
66
+ the parent explicitly asks for debug/UAT output.
67
67
 
68
68
  When reporting branch runtime proof, use this shape under
69
69
  `watchNarration.workerDetails.messageDraftBuilder`:
@@ -211,11 +211,7 @@
211
211
  "token fill rules",
212
212
  "rendered sample"
213
213
  ],
214
- "optionalProducesArtifacts": [
215
- "message-validation.md",
216
- "message-prep.md",
217
- "message-candidate-drafts.md"
218
- ],
214
+ "optionalProducesArtifacts": [],
219
215
  "ownership": "proof inventory, token strategy, angle drafting, skeptical-prospect review, and selected winner only",
220
216
  "codex": {
221
217
  "description": "Message Draft Builder for campaign-backed template proposals after confirm_lead_list imports a non-empty bounded review batch.",
package/dist/index-dev.js CHANGED
File without changes
package/dist/index.js CHANGED
File without changes
package/dist/server.js CHANGED
@@ -2,35 +2,36 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
4
  import { getSkillByName, listSkills } from "./skills.js";
5
- import { authToolDefinitions, getAuthStatus } from "./tools/auth.js";
6
- import { blueprintCommitToolDefinitions, handleAddColumn, handleCommitBlueprint, } from "./tools/blueprint-commit.js";
7
- import { bootstrapCreateCampaign, bootstrapToolDefinitions, } from "./tools/bootstrap.js";
8
- import { campaignToolDefinitions, createCampaign, getCampaign, getCampaignMessagesPreview, getCampaigns, pauseCampaign, startCampaign, updateCampaign, updateCampaignBrief, } from "./tools/campaigns.js";
9
- import { cellToolDefinitions, queueCells, updateCell } from "./tools/cells.js";
10
- import { handleStartCliLogin, handleWaitForCliLogin, startCliLoginToolDef, waitForCliLoginToolDef, } from "./tools/cli-login.js";
11
- import { contextToolDefinitions, getCampaignContext, hydrateCampaignContextFromCampaign, markCampaignContextDirty, } from "./tools/context.js";
12
- import { addToCommentCampaign, addToConnectionCampaign, addToInmailCampaign, directCampaignToolDefinitions, getEngagedPosts, getOrCreateDirectCampaignTable, pauseDirectCampaign, startDirectCampaign, } from "./tools/direct-campaigns.js";
13
- import { bootstrapEngage, bootstrapEngageMulti, engageBootstrapToolDefinitions, } from "./tools/engage-bootstrap.js";
14
- import { engageDiscoveryToolDefinitions, searchEngagementPosts, } from "./tools/engage-discovery.js";
15
- import { copySenderConfigTool, engageMemoryToolDefinitions, getEngageMemoryTool, migrateFlatConfigsTool, recordEngageProvenSearchTool, setEngageStyleGuideTool, upsertEngageTrackedPersonTool, } from "./tools/engage-memory.js";
16
- import { engageStateToolDefinitions, getEngageStateTool, setEngageStateTool, } from "./tools/engage-state.js";
17
- import { bulkEnrichWithProspeo, enrichmentToolDefinitions, enrichWithProspeo, getProspeoCredits, } from "./tools/enrichment.js";
18
- import { frameworkToolDefinitions, getCampaignFramework, } from "./tools/framework.js";
19
- import { cancelLeadImport, confirmLeadList, getProviderPrompt, importLeads, leadToolDefinitions, loadCsvDomains, loadCsvLinkedinLeads, lookupSalesNavFilter, saveDomainFilters, searchApollo, searchProspeo, searchSalesNav, searchSignals, selectPromisingPosts, setHeadlineICPCriteria, } from "./tools/leads.js";
20
- import { fetchCompany, fetchCompanyPosts, fetchLinkedInPosts, fetchLinkedInProfile, fetchPostEngagers, getLinkedInProfile, getUserPosts, linkedinToolDefinitions, } from "./tools/linkedin.js";
21
- import { getCampaignNavigationState, navigationToolDefinitions, } from "./tools/navigation.js";
22
- import { addOnDemandLeads, createOnDemandCampaign, createOnDemandTable, initOnDemandSequence, onDemandToolDefinitions, pauseOnDemandCampaign, startOnDemandCampaign, } from "./tools/one-off.js";
23
- import { processingToolDefinitions, upsertRubric } from "./tools/processing.js";
24
- import { completeSenderResearch, getMessagePrompt, getPostFindLeadsScoutRegistry, getSourceScoutRegistry, getSubskillAsset, getSubskillPrompt, listSubskillPrompts, promptToolDefinitions, searchSubskillPrompts, } from "./tools/prompts.js";
25
- import { readinessToolDefinitions, waitForCampaignTableReady, waitForLeadListReady, } from "./tools/readiness.js";
26
- import { getRows, getTableRows, getTableRowsMinimal, rowToolDefinitions, } from "./tools/rows.js";
27
- import { addRubricItem, checkRubric, deleteRubricItem, draftRubrics, rubricToolDefinitions, saveRubrics, selectNecessaryRubrics, updateRubricItem, waitForRubricResults, } from "./tools/rubrics.js";
28
- import { getSender, listSenders, senderToolDefinitions, } from "./tools/senders.js";
29
- import { attachRecommendedSequence, attachSequence, createWorkflowTable, sequencerToolDefinitions, } from "./tools/sequencer.js";
30
- import { listTables, tableToolDefinitions } from "./tools/tables.js";
31
- import { handleVerifyTableRow, verifyRowToolDefinitions, } from "./tools/verify-row.js";
5
+ import { getAuthStatus } from "./tools/auth.js";
6
+ import { handleAddColumn, handleCommitBlueprint, } from "./tools/blueprint-commit.js";
7
+ import { bootstrapCreateCampaign } from "./tools/bootstrap.js";
8
+ import { createCampaign, getCampaign, getCampaignMessagesPreview, getCampaigns, pauseCampaign, startCampaign, updateCampaign, updateCampaignBrief, } from "./tools/campaigns.js";
9
+ import { queueCells, updateCell } from "./tools/cells.js";
10
+ import { handleStartCliLogin, handleWaitForCliLogin, } from "./tools/cli-login.js";
11
+ import { getCampaignContext, hydrateCampaignContextFromCampaign, markCampaignContextDirty, } from "./tools/context.js";
12
+ import { addToCommentCampaign, addToConnectionCampaign, addToInmailCampaign, getEngagedPosts, getOrCreateDirectCampaignTable, pauseDirectCampaign, startDirectCampaign, } from "./tools/direct-campaigns.js";
13
+ import { bootstrapEngage, bootstrapEngageMulti, } from "./tools/engage-bootstrap.js";
14
+ import { searchEngagementPosts } from "./tools/engage-discovery.js";
15
+ import { copySenderConfigTool, getEngageMemoryTool, migrateFlatConfigsTool, recordEngageProvenSearchTool, setEngageStyleGuideTool, upsertEngageTrackedPersonTool, } from "./tools/engage-memory.js";
16
+ import { getEngageStateTool, setEngageStateTool, } from "./tools/engage-state.js";
17
+ import { bulkEnrichWithProspeo, enrichWithProspeo, getProspeoCredits, } from "./tools/enrichment.js";
18
+ import { getCampaignFramework } from "./tools/framework.js";
19
+ import { cancelLeadImport, confirmLeadList, getProviderPrompt, importLeads, loadCsvDomains, loadCsvLinkedinLeads, lookupSalesNavFilter, saveDomainFilters, searchApollo, searchProspeo, searchSalesNav, searchSignals, selectPromisingPosts, setHeadlineICPCriteria, } from "./tools/leads.js";
20
+ import { fetchCompany, fetchCompanyPosts, fetchLinkedInPosts, fetchLinkedInProfile, fetchPostEngagers, getLinkedInProfile, getUserPosts, } from "./tools/linkedin.js";
21
+ import { getCampaignNavigationState } from "./tools/navigation.js";
22
+ import { addOnDemandLeads, createOnDemandCampaign, createOnDemandTable, initOnDemandSequence, pauseOnDemandCampaign, startOnDemandCampaign, } from "./tools/one-off.js";
23
+ import { upsertRubric } from "./tools/processing.js";
24
+ import { completeSenderResearch, getMessagePrompt, getPostFindLeadsScoutRegistry, getSourceScoutRegistry, getSubskillAsset, getSubskillPrompt, listSubskillPrompts, searchSubskillPrompts, } from "./tools/prompts.js";
25
+ import { waitForCampaignTableReady, waitForLeadListReady, } from "./tools/readiness.js";
26
+ import { allTools } from "./tools/registry.js";
27
+ import { getRows, getTableRows, getTableRowsMinimal } from "./tools/rows.js";
28
+ import { addRubricItem, checkRubric, deleteRubricItem, draftRubrics, saveRubrics, selectNecessaryRubrics, updateRubricItem, waitForRubricResults, } from "./tools/rubrics.js";
29
+ import { getSender, listSenders } from "./tools/senders.js";
30
+ import { attachRecommendedSequence, attachSequence, createWorkflowTable, } from "./tools/sequencer.js";
31
+ import { listTables } from "./tools/tables.js";
32
+ import { handleVerifyTableRow } from "./tools/verify-row.js";
32
33
  import { sanitizeWatchUrlsForMcpResult } from "./tools/watch-url-security.js";
33
- import { addTeammate, createWorkspace, getActiveWorkspace, listWorkspaces, setActiveWorkspace, workspaceToolDefinitions, } from "./tools/workspaces.js";
34
+ import { addTeammate, createWorkspace, getActiveWorkspace, listWorkspaces, setActiveWorkspace, } from "./tools/workspaces.js";
34
35
  import { checkForUpdates, logUpdateNotice } from "./update-check.js";
35
36
  const server = new Server({
36
37
  name: "sellable-mcp",
@@ -41,37 +42,6 @@ const server = new Server({
41
42
  prompts: {},
42
43
  },
43
44
  });
44
- const allTools = [
45
- ...campaignToolDefinitions,
46
- ...authToolDefinitions,
47
- startCliLoginToolDef,
48
- waitForCliLoginToolDef,
49
- ...bootstrapToolDefinitions,
50
- ...engageBootstrapToolDefinitions,
51
- ...engageDiscoveryToolDefinitions,
52
- ...workspaceToolDefinitions,
53
- ...frameworkToolDefinitions,
54
- ...contextToolDefinitions,
55
- ...navigationToolDefinitions,
56
- ...leadToolDefinitions,
57
- ...enrichmentToolDefinitions,
58
- ...processingToolDefinitions,
59
- ...rubricToolDefinitions,
60
- ...readinessToolDefinitions,
61
- ...rowToolDefinitions,
62
- ...cellToolDefinitions,
63
- ...promptToolDefinitions,
64
- ...linkedinToolDefinitions,
65
- ...onDemandToolDefinitions,
66
- ...directCampaignToolDefinitions,
67
- ...senderToolDefinitions,
68
- ...engageStateToolDefinitions,
69
- ...engageMemoryToolDefinitions,
70
- ...sequencerToolDefinitions,
71
- ...tableToolDefinitions,
72
- ...blueprintCommitToolDefinitions,
73
- ...verifyRowToolDefinitions,
74
- ];
75
45
  function parseOptionalNumber(value) {
76
46
  if (typeof value === "number" && Number.isFinite(value))
77
47
  return value;
@@ -22,7 +22,7 @@ function appendUpdateNotice(notice, update) {
22
22
  export const authToolDefinitions = [
23
23
  {
24
24
  name: "get_auth_status",
25
- description: "Verify Sellable authentication early. Call this before any campaign work. Also performs a cached Sellable package update check. Returns ok=false with clear guidance when the token is missing, revoked, or the active workspace is not set. IMPORTANT: When auth succeeds, ALWAYS tell the user the _userNotice message so they know which workspace is active and whether an update is available. CRITICAL: When auth FAILS with a missing/invalid token (error.type is 'config' or 'auth'), the response includes an `agentInstruction` field with the verbatim FTUX magic-link signup flow. Follow agentInstruction VERBATIM. Do NOT show the user error.guidance — that's a fallback for non-interactive contexts only.",
25
+ description: "Verify Sellable authentication early. Call this before any campaign work. Also performs a cached Sellable package update check. Returns ok=false with clear guidance when the token is missing, revoked, or the active workspace is not set. When auth succeeds, use _userNotice when workspace context is useful; it is written in customer-facing product language and may include update guidance. CRITICAL: When auth FAILS with a missing/invalid token (error.type is 'config' or 'auth'), the response includes an `agentInstruction` field with the verbatim FTUX magic-link signup flow. Follow agentInstruction VERBATIM. Do NOT show the user error.guidance — that's a fallback for non-interactive contexts only.",
26
26
  inputSchema: {
27
27
  type: "object",
28
28
  properties: {},
@@ -92,8 +92,8 @@ export async function getAuthStatus() {
92
92
  const envLabel = config.activeEnvName || null;
93
93
  const wsLabel = workspaceName || activeWorkspaceId;
94
94
  const notice = envLabel
95
- ? `Workspace: ${wsLabel} (${envLabel})`
96
- : `Workspace: ${wsLabel}`;
95
+ ? `I’m building this in ${wsLabel} (${envLabel}).`
96
+ : `I’m building this in ${wsLabel}.`;
97
97
  return {
98
98
  ok: true,
99
99
  configPath,
@@ -138,8 +138,8 @@ export async function getAuthStatus() {
138
138
  "5) Call `mcp__sellable__wait_for_cli_login({ sessionId })` using the sessionId returned by start_cli_login. " +
139
139
  "6) If the result is `error.type === 'tool_timeout_guard'`, IMMEDIATELY re-call wait_for_cli_login with the SAME sessionId — do not narrate, do not call start_cli_login again. Loop until you get a different result. " +
140
140
  "7) On `ok: true`, the user is signed in and `~/.sellable/config.json` has been written. Branch on `isReturningUser`: " +
141
- "if true, say `You're in — {activeWorkspaceName} workspace, ready to roll.\\n\\nNow — paste the LinkedIn profile URL of the person you'll be sending campaigns from. Usually that's you (the founder), or whoever's voice the messages should sound like.\\n\\ne.g. https://www.linkedin.com/in/your-handle`; " +
142
- "if false, say `You're set up — your {activeWorkspaceName} workspace is ready.\\n\\nNow — paste the LinkedIn profile URL of the person you'll be sending campaigns from. Usually that's you (the founder), or whoever's voice the messages should sound like.\\n\\ne.g. https://www.linkedin.com/in/your-handle`";
141
+ "if true, say `You're in — {activeWorkspaceName} workspace, ready to roll.\\n\\nNow — paste the LinkedIn profile or company website for the client/company this campaign is for. I’ll use that to resolve the campaign identity before we pick the target, offer, proof, and lead source.\\n\\ne.g. https://example.com or https://www.linkedin.com/in/client-handle`; " +
142
+ "if false, say `You're set up — your {activeWorkspaceName} workspace is ready.\\n\\nNow — paste the LinkedIn profile or company website for the client/company this campaign is for. I’ll use that to resolve the campaign identity before we pick the target, offer, proof, and lead source.\\n\\ne.g. https://example.com or https://www.linkedin.com/in/client-handle`";
143
143
  if (error instanceof SellableApiError && error.isAuthError) {
144
144
  return {
145
145
  ...base,
@@ -128,8 +128,6 @@ export type ImportLeadsInput = {
128
128
  maxPostsToScrape?: number;
129
129
  rubricGuidelines?: string[];
130
130
  };
131
- type ImportLeadProvider = NonNullable<ImportLeadsInput["provider"]>;
132
- type Phase126ImportProvider = Exclude<ImportLeadProvider, "apollo">;
133
131
  export type CancelLeadImportInput = {
134
132
  campaignOfferId: string;
135
133
  tableId: string;
@@ -2349,6 +2347,45 @@ export declare function searchSignals(input: SignalSearchInput): Promise<{
2349
2347
  notes: string[];
2350
2348
  }>;
2351
2349
  export declare function importLeads(input: ImportLeadsInput): Promise<{
2350
+ provider: string;
2351
+ leadListId: string;
2352
+ existingLeadListId: string;
2353
+ reusedExistingSourceList: boolean;
2354
+ message: string;
2355
+ suggestedToolCalls: ({
2356
+ tool: string;
2357
+ args: {
2358
+ campaignOfferId: string;
2359
+ leadListId: string;
2360
+ provider: string;
2361
+ sourceLeadListId?: undefined;
2362
+ confirmed?: undefined;
2363
+ };
2364
+ } | {
2365
+ tool: string;
2366
+ args: {
2367
+ campaignOfferId: string;
2368
+ sourceLeadListId: string;
2369
+ confirmed: boolean;
2370
+ leadListId?: undefined;
2371
+ provider?: undefined;
2372
+ };
2373
+ })[];
2374
+ error?: undefined;
2375
+ needsModeSelection?: undefined;
2376
+ modeOptions?: undefined;
2377
+ jobId?: undefined;
2378
+ estimatedEngagers?: undefined;
2379
+ selectedPostCount?: undefined;
2380
+ availableSelectedPostCount?: undefined;
2381
+ targetEngagerCount?: undefined;
2382
+ maxPostsToScrape?: undefined;
2383
+ limitedSelectedPosts?: undefined;
2384
+ targetLeadCount?: undefined;
2385
+ existingCount?: undefined;
2386
+ createdLeadList?: undefined;
2387
+ jobResult?: undefined;
2388
+ } | {
2352
2389
  error: string;
2353
2390
  message: string;
2354
2391
  existingLeadListId: string;
@@ -2373,6 +2410,7 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
2373
2410
  }[];
2374
2411
  provider?: undefined;
2375
2412
  leadListId?: undefined;
2413
+ reusedExistingSourceList?: undefined;
2376
2414
  needsModeSelection?: undefined;
2377
2415
  modeOptions?: undefined;
2378
2416
  jobId?: undefined;
@@ -2387,7 +2425,7 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
2387
2425
  createdLeadList?: undefined;
2388
2426
  jobResult?: undefined;
2389
2427
  } | {
2390
- provider: Phase126ImportProvider;
2428
+ provider: "signal-discovery" | "sales-nav" | "prospeo";
2391
2429
  leadListId: string;
2392
2430
  existingLeadListId: string;
2393
2431
  needsModeSelection: boolean;
@@ -2412,6 +2450,7 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
2412
2450
  rubricGuidelines?: string[];
2413
2451
  };
2414
2452
  }[];
2453
+ reusedExistingSourceList?: undefined;
2415
2454
  error?: undefined;
2416
2455
  jobId?: undefined;
2417
2456
  estimatedEngagers?: undefined;
@@ -2436,9 +2475,10 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
2436
2475
  limitedSelectedPosts: boolean;
2437
2476
  targetLeadCount: number | null;
2438
2477
  message: string;
2439
- error?: undefined;
2440
2478
  existingLeadListId?: undefined;
2479
+ reusedExistingSourceList?: undefined;
2441
2480
  suggestedToolCalls?: undefined;
2481
+ error?: undefined;
2442
2482
  needsModeSelection?: undefined;
2443
2483
  modeOptions?: undefined;
2444
2484
  existingCount?: undefined;
@@ -2471,6 +2511,7 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
2471
2511
  rubricGuidelines?: string[];
2472
2512
  };
2473
2513
  }[];
2514
+ reusedExistingSourceList?: undefined;
2474
2515
  error?: undefined;
2475
2516
  jobId?: undefined;
2476
2517
  estimatedEngagers?: undefined;
@@ -2490,9 +2531,10 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
2490
2531
  jobId: string | undefined;
2491
2532
  targetLeadCount: number | null;
2492
2533
  message: string;
2493
- error?: undefined;
2494
2534
  existingLeadListId?: undefined;
2535
+ reusedExistingSourceList?: undefined;
2495
2536
  suggestedToolCalls?: undefined;
2537
+ error?: undefined;
2496
2538
  needsModeSelection?: undefined;
2497
2539
  modeOptions?: undefined;
2498
2540
  estimatedEngagers?: undefined;
@@ -504,6 +504,39 @@ function buildFilterChoiceWatchNarration({ sourceLeadCount, reviewRowCount, }) {
504
504
  },
505
505
  };
506
506
  }
507
+ function buildSignalDiscoverySearchWatchNarration() {
508
+ return {
509
+ stage: "find-leads",
510
+ headline: "Searching Signal Discovery",
511
+ visibleState: "The browser is showing Signal Discovery while LinkedIn post searches run.",
512
+ agentIntent: "Codex is searching the approved post themes and will ask before scraping selected engagers into a source list.",
513
+ nextAction: "Review selected posts before scraping",
514
+ safety: "Search only stores post candidates; it does not import leads, enrich, generate messages, create a sequence, or send.",
515
+ };
516
+ }
517
+ function buildSignalDiscoveryResultsWatchNarration(postsReturned) {
518
+ const postCopy = typeof postsReturned === "number"
519
+ ? `${postsReturned.toLocaleString("en-US")} post${postsReturned === 1 ? "" : "s"} matched the approved search themes and are visible in Signal Discovery.`
520
+ : "Signal Discovery results are visible for review.";
521
+ return {
522
+ stage: "find-leads",
523
+ headline: "Review Signal Discovery posts",
524
+ visibleState: postCopy,
525
+ agentIntent: "Codex is promoting the strongest posts, then it will ask before scraping the selected engager pool.",
526
+ nextAction: "Approve selected posts before scraping",
527
+ safety: "Post results do not import leads, enrich, generate messages, create a sequence, or send.",
528
+ };
529
+ }
530
+ function buildSelectedPostApprovalWatchNarration(selectedPostCount) {
531
+ return {
532
+ stage: "find-leads",
533
+ headline: "Approve selected-post scrape",
534
+ visibleState: `${selectedPostCount.toLocaleString("en-US")} LinkedIn post${selectedPostCount === 1 ? "" : "s"} selected in Signal Discovery.`,
535
+ agentIntent: "Codex is asking before scraping this selected engager pool into a source list and cloning only the bounded review batch into the campaign.",
536
+ nextAction: `Scrape ${selectedPostCount.toLocaleString("en-US")} selected post${selectedPostCount === 1 ? "" : "s"} for the bounded review batch`,
537
+ safety: "Post selection does not import leads, enrich, generate messages, create a sequence, or send.",
538
+ };
539
+ }
507
540
  function normalizeImportProvider(provider) {
508
541
  if (provider === "apollo-ai" || provider === "apollo")
509
542
  return "apollo";
@@ -1951,9 +1984,20 @@ export async function searchSignals(input) {
1951
1984
  });
1952
1985
  const api = getApi();
1953
1986
  const { campaignOfferId, headlineICPCriteria, rubricGuidelines } = input;
1987
+ const searchCurrentStep = input.currentStep === null
1988
+ ? undefined
1989
+ : (input.currentStep ?? "signal-discovery");
1954
1990
  const effectiveHeadlineICPCriteria = headlineICPCriteria && headlineICPCriteria.length > 0
1955
1991
  ? headlineICPCriteria
1956
1992
  : rubricGuidelines;
1993
+ if (campaignOfferId) {
1994
+ await api.put(`/api/v2/campaign-offers/${campaignOfferId}`, {
1995
+ leadSourceType: "new",
1996
+ leadSourceProvider: "signal-discovery",
1997
+ ...(searchCurrentStep ? { currentStep: searchCurrentStep } : {}),
1998
+ watchNarration: buildSignalDiscoverySearchWatchNarration(),
1999
+ });
2000
+ }
1957
2001
  // Persist criteria early so user take-over has filters ready.
1958
2002
  if (campaignOfferId && effectiveHeadlineICPCriteria?.length) {
1959
2003
  await api.patch(`/api/v3/campaigns/${campaignOfferId}/signal-discovery/posts`, {
@@ -1964,7 +2008,14 @@ export async function searchSignals(input) {
1964
2008
  });
1965
2009
  }
1966
2010
  const response = await api.post(`/api/v1/signal-discovery/search-signals`, input);
1967
- return summarizeSignalSearchResponse(response);
2011
+ const summary = summarizeSignalSearchResponse(response);
2012
+ if (campaignOfferId) {
2013
+ await api.put(`/api/v2/campaign-offers/${campaignOfferId}`, {
2014
+ ...(searchCurrentStep ? { currentStep: searchCurrentStep } : {}),
2015
+ watchNarration: buildSignalDiscoveryResultsWatchNarration(summary.postsReturned),
2016
+ });
2017
+ }
2018
+ return summary;
1968
2019
  }
1969
2020
  export async function importLeads(input) {
1970
2021
  const api = getApi();
@@ -2009,6 +2060,35 @@ export async function importLeads(input) {
2009
2060
  const shouldSetCurrentStep = typeof effectiveCurrentStep === "string" && effectiveCurrentStep.length > 0;
2010
2061
  const normalizedMode = mode === "add" || mode === "replace" ? mode : undefined;
2011
2062
  assertPhase126ImportProvider(provider, campaignOfferId);
2063
+ if (provider === "signal-discovery" &&
2064
+ !sourceLeadListId &&
2065
+ campaignSelectedLeadListId) {
2066
+ return {
2067
+ provider: "signal-discovery",
2068
+ leadListId: campaignSelectedLeadListId,
2069
+ existingLeadListId: campaignSelectedLeadListId,
2070
+ reusedExistingSourceList: true,
2071
+ message: "A Signal Discovery source list already exists for the approved selected-post scrape. Reuse it and continue with wait_for_lead_list_ready or confirm_lead_list; do not ask add/replace and do not rescrape unless the user approves a different selected-post scrape.",
2072
+ suggestedToolCalls: [
2073
+ {
2074
+ tool: "wait_for_lead_list_ready",
2075
+ args: {
2076
+ campaignOfferId,
2077
+ leadListId: campaignSelectedLeadListId,
2078
+ provider: "signal-discovery",
2079
+ },
2080
+ },
2081
+ {
2082
+ tool: "confirm_lead_list",
2083
+ args: {
2084
+ campaignOfferId,
2085
+ sourceLeadListId: campaignSelectedLeadListId,
2086
+ confirmed: true,
2087
+ },
2088
+ },
2089
+ ],
2090
+ };
2091
+ }
2012
2092
  // If no sourceLeadListId but campaign has one selected, handle mode selection
2013
2093
  if (!sourceLeadListId && campaignSelectedLeadListId) {
2014
2094
  // If mode is provided but sourceLeadListId is missing, remind LLM to pass it
@@ -2614,7 +2694,7 @@ export function getProviderPrompt(input) {
2614
2694
  }
2615
2695
  export async function selectPromisingPosts(input) {
2616
2696
  const api = getApi();
2617
- const { campaignOfferId, selections, headlineICPCriteria, currentStep, selectionMode, mode, } = input;
2697
+ const { campaignOfferId, selections, headlineICPCriteria, selectionMode, mode, } = input;
2618
2698
  const effectiveMode = selectionMode ?? mode ?? "add";
2619
2699
  // Update post selections via API
2620
2700
  const postIds = selections.map((s) => s.postId);
@@ -2642,18 +2722,16 @@ export async function selectPromisingPosts(input) {
2642
2722
  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.",
2643
2723
  };
2644
2724
  }
2645
- // Update currentStep if provided (via v2 endpoint)
2646
- if (currentStep) {
2647
- await api.put(`/api/v2/campaign-offers/${campaignOfferId}`, {
2648
- currentStep,
2649
- });
2650
- }
2725
+ await api.put(`/api/v2/campaign-offers/${campaignOfferId}`, {
2726
+ currentStep: "signal-discovery",
2727
+ watchNarration: buildSelectedPostApprovalWatchNarration(selectionResult.selectedCount),
2728
+ });
2651
2729
  return {
2652
2730
  success: true,
2653
2731
  selectedCount: selectionResult.selectedCount,
2654
2732
  unselectedCount: selectionResult.unselectedCount,
2655
2733
  criteriaCount: selectionResult.criteriaCount,
2656
- message: `Selected ${selectionResult.selectedCount} posts with ${selectionResult.criteriaCount} ICP criteria (persisted). Criteria are already saved to the campaign. If the user has already approved the lead-source decision for create-campaign-v2, call import_leads with provider: "signal-discovery", confirmed: true, the approved source-capacity targetLeadCount, and targetEngagerCount/postsNeeded from the approval math. Otherwise ask the user to confirm before importing.`,
2734
+ message: `Selected ${selectionResult.selectedCount} posts with ${selectionResult.criteriaCount} ICP criteria (persisted). Criteria are already saved to the campaign. Ask the user to approve scraping these selected posts before calling import_leads; the earlier source-plan approval only authorized search/scouting.`,
2657
2735
  };
2658
2736
  }
2659
2737
  export async function setHeadlineICPCriteria(input) {
@@ -2666,15 +2744,14 @@ export async function setHeadlineICPCriteria(input) {
2666
2744
  headlineICPCriteria,
2667
2745
  rubricGuidelines: headlineICPCriteria,
2668
2746
  });
2669
- // Update currentStep if provided (via v2 endpoint)
2670
- if (currentStep) {
2671
- await api.put(`/api/v2/campaign-offers/${campaignOfferId}`, {
2672
- currentStep,
2673
- });
2674
- }
2747
+ const criteriaCurrentStep = currentStep === null ? undefined : (currentStep ?? "signal-discovery");
2748
+ await api.put(`/api/v2/campaign-offers/${campaignOfferId}`, {
2749
+ ...(criteriaCurrentStep ? { currentStep: criteriaCurrentStep } : {}),
2750
+ watchNarration: buildSignalDiscoveryResultsWatchNarration(),
2751
+ });
2675
2752
  return {
2676
2753
  success: true,
2677
2754
  criteriaCount: headlineICPCriteria.length,
2678
- message: `Prepared ${headlineICPCriteria.length} ICP criteria (persisted). Call import_leads with provider: "signal-discovery". Criteria are already saved to the campaign.`,
2755
+ message: `Prepared ${headlineICPCriteria.length} ICP criteria (persisted). Select posts and ask for selected-post scrape approval before calling import_leads with provider: "signal-discovery".`,
2679
2756
  };
2680
2757
  }
@@ -537,6 +537,9 @@ export function computeCampaignNavigationStateFromCampaign(campaign, options) {
537
537
  computedStep = isRunningCampaign(campaign) ? "running" : "send";
538
538
  }
539
539
  const stepForHeadless = blockedAt ?? computedStep;
540
+ const waitingOnMessageTemplateAfterFilters = blockedAt === "messages" &&
541
+ campaign.enableICPFilters === true &&
542
+ hasActiveRubrics(campaign);
540
543
  const expectedHeadlessStep = stepForHeadless === "campaign-created"
541
544
  ? "create-offer"
542
545
  : stepForHeadless === "provider-search"
@@ -544,7 +547,9 @@ export function computeCampaignNavigationStateFromCampaign(campaign, options) {
544
547
  : stepForHeadless === "filter-rules"
545
548
  ? "create-icp-rubric"
546
549
  : stepForHeadless === "messages"
547
- ? "messages"
550
+ ? waitingOnMessageTemplateAfterFilters
551
+ ? "apply-icp-rubric"
552
+ : "messages"
548
553
  : stepForHeadless === "settings"
549
554
  ? "settings"
550
555
  : stepForHeadless === "sequence"