@sellable/mcp 0.1.80 → 0.1.82

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.
Files changed (36) hide show
  1. package/agents/post-find-leads-filter-scout.md +19 -8
  2. package/agents/post-find-leads-message-scout.md +21 -6
  3. package/agents/registry.json +33 -18
  4. package/agents/source-scout-linkedin-engagement.md +10 -2
  5. package/agents/source-scout-prospeo-contact.md +9 -2
  6. package/agents/source-scout-sales-nav.md +10 -2
  7. package/dist/server.js +7 -1
  8. package/dist/tools/bootstrap.js +2 -2
  9. package/dist/tools/campaigns.d.ts +12 -0
  10. package/dist/tools/campaigns.js +40 -5
  11. package/dist/tools/context.d.ts +10 -4
  12. package/dist/tools/context.js +8 -4
  13. package/dist/tools/leads.d.ts +6 -3
  14. package/dist/tools/leads.js +39 -43
  15. package/dist/tools/navigation.d.ts +28 -1
  16. package/dist/tools/navigation.js +220 -10
  17. package/dist/tools/prompts.js +1 -1
  18. package/dist/tools/provider-preflight.js +2 -3
  19. package/dist/tools/readiness.d.ts +31 -6
  20. package/dist/tools/readiness.js +6 -1
  21. package/dist/tools/rubrics.d.ts +2 -0
  22. package/dist/tools/rubrics.js +2 -0
  23. package/package.json +1 -1
  24. package/skills/create-campaign/SKILL.md +24 -12
  25. package/skills/create-campaign-v2/SKILL.md +166 -114
  26. package/skills/create-campaign-v2/SOUL.md +19 -11
  27. package/skills/create-campaign-v2/core/auto-execute.yaml +14 -13
  28. package/skills/create-campaign-v2/core/flow.v2.json +964 -310
  29. package/skills/create-campaign-v2/references/approval-gate-framing.md +64 -32
  30. package/skills/create-campaign-v2/references/filter-leads.md +8 -0
  31. package/skills/create-campaign-v2/references/final-handoff-contract.md +55 -20
  32. package/skills/create-campaign-v2/references/lead-validation-preview.md +30 -17
  33. package/skills/create-campaign-v2/references/step-13-import-leads.md +62 -17
  34. package/skills/create-campaign-v2/references/validation-criteria.md +10 -5
  35. package/skills/create-campaign-v2/references/watch-link-handoff.md +72 -72
  36. package/skills/create-campaign-v2-tail/SKILL.md +123 -92
@@ -18,6 +18,7 @@ const defaultLeadImportLimits = {
18
18
  apollo: { maxImportCount: 2500 },
19
19
  "sales-nav": { maxImportCount: 2500 },
20
20
  prospeo: { maxImportCount: 10000 },
21
+ "signal-discovery": { maxImportCount: 2500 },
21
22
  };
22
23
  const defaultSignalDiscoveryConfig = {
23
24
  selection: {
@@ -388,6 +389,25 @@ function summarizeSignalSearchResponse(response) {
388
389
  ],
389
390
  };
390
391
  }
392
+ function normalizeImportProvider(provider) {
393
+ if (provider === "apollo-ai" || provider === "apollo")
394
+ return "apollo";
395
+ if (provider === "sales-nav")
396
+ return "sales-nav";
397
+ if (provider === "prospeo")
398
+ return "prospeo";
399
+ if (provider === "signal-discovery")
400
+ return "signal-discovery";
401
+ return undefined;
402
+ }
403
+ function assertPhase126ImportProvider(provider, campaignOfferId) {
404
+ if (!provider) {
405
+ throw new Error(`import_leads requires a supported leadSourceProvider for campaign ${campaignOfferId}. Provide provider or set campaign.leadSourceProvider to sales-nav, prospeo, or signal-discovery, then call get_provider_prompt({ provider, campaignOfferId }).`);
406
+ }
407
+ if (provider === "apollo") {
408
+ throw new Error(`Apollo is not offered as a Phase 126 post-mint source for campaign ${campaignOfferId}. Choose sales-nav, prospeo, or signal-discovery; call get_provider_prompt({ provider, campaignOfferId }), then retry import_leads.`);
409
+ }
410
+ }
391
411
  export const leadToolDefinitions = [
392
412
  {
393
413
  name: "get_provider_prompt",
@@ -985,7 +1005,7 @@ export const leadToolDefinitions = [
985
1005
  },
986
1006
  {
987
1007
  name: "confirm_lead_list",
988
- description: "After the user confirms the lead list looks good, import it into the campaign table (clone) and update selectedLeadListId to the campaign table. Recommended post-confirm order: update_campaign(currentStep='filter-choice') -> wait_for_campaign_table_ready -> get_rows_minimal.",
1008
+ description: "After the user confirms the lead list looks good, import it into the campaign table (clone). selectedLeadListId remains the source lead list; workflowTableId is the campaign table. Recommended post-confirm order: update_campaign(currentStep='filter-choice') -> wait_for_campaign_table_ready -> get_rows_minimal.",
989
1009
  inputSchema: {
990
1010
  type: "object",
991
1011
  properties: {
@@ -1841,17 +1861,7 @@ export async function importLeads(input) {
1841
1861
  // Pull campaign once when we need provider or to determine default step behavior.
1842
1862
  const campaign = await api.get(`/api/v2/campaign-offers/${campaignOfferId}`);
1843
1863
  if (!provider) {
1844
- const leadSourceProvider = campaign.leadSourceProvider;
1845
- provider =
1846
- leadSourceProvider === "apollo-ai" || leadSourceProvider === "apollo"
1847
- ? "apollo"
1848
- : leadSourceProvider === "sales-nav"
1849
- ? "sales-nav"
1850
- : leadSourceProvider === "prospeo"
1851
- ? "prospeo"
1852
- : leadSourceProvider === "signal-discovery"
1853
- ? "signal-discovery"
1854
- : undefined;
1864
+ provider = normalizeImportProvider(campaign.leadSourceProvider);
1855
1865
  }
1856
1866
  if (currentStep === undefined) {
1857
1867
  campaignCurrentStep = campaign.currentStep ?? null;
@@ -1875,6 +1885,7 @@ export async function importLeads(input) {
1875
1885
  : currentStep;
1876
1886
  const shouldSetCurrentStep = typeof effectiveCurrentStep === "string" && effectiveCurrentStep.length > 0;
1877
1887
  const normalizedMode = mode === "add" || mode === "replace" ? mode : undefined;
1888
+ assertPhase126ImportProvider(provider, campaignOfferId);
1878
1889
  // If no sourceLeadListId but campaign has one selected, handle mode selection
1879
1890
  if (!sourceLeadListId && campaignSelectedLeadListId) {
1880
1891
  // If mode is provided but sourceLeadListId is missing, remind LLM to pass it
@@ -1896,7 +1907,7 @@ export async function importLeads(input) {
1896
1907
  }
1897
1908
  // No mode provided - ask user to choose
1898
1909
  return {
1899
- provider: provider || "apollo",
1910
+ provider,
1900
1911
  leadListId: campaignSelectedLeadListId,
1901
1912
  existingLeadListId: campaignSelectedLeadListId,
1902
1913
  needsModeSelection: true,
@@ -1922,22 +1933,14 @@ export async function importLeads(input) {
1922
1933
  ],
1923
1934
  };
1924
1935
  }
1925
- const normalizedProvider = provider === "sales-nav" || provider === "prospeo" ? provider : "apollo";
1926
- const providerForGuard = provider === "sales-nav" ||
1927
- provider === "prospeo" ||
1928
- provider === "signal-discovery"
1929
- ? provider
1930
- : "apollo";
1931
1936
  assertProviderPromptLoaded({
1932
- provider: providerForGuard,
1937
+ provider,
1933
1938
  campaignOfferId,
1934
1939
  });
1935
- const maxImportCount = getMaxImportCount(normalizedProvider);
1940
+ const maxImportCount = getMaxImportCount(provider);
1936
1941
  const normalizedTargetLeadCount = normalizeTargetLeadCount(targetLeadCount, maxImportCount);
1937
1942
  const requestedLeadCount = normalizedTargetLeadCount ??
1938
- (normalizedProvider === "sales-nav" || normalizedProvider === "prospeo"
1939
- ? 100
1940
- : undefined);
1943
+ (provider === "sales-nav" || provider === "prospeo" ? 100 : undefined);
1941
1944
  const cappedTargetLeadCount = requestedLeadCount !== undefined
1942
1945
  ? Math.min(requestedLeadCount, maxImportCount)
1943
1946
  : undefined;
@@ -1968,7 +1971,7 @@ export async function importLeads(input) {
1968
1971
  }
1969
1972
  }
1970
1973
  if (selectedPosts.length === 0) {
1971
- throw new Error("No posts selected. Call select_promising_posts first.");
1974
+ throw new Error("No usable Signal Discovery posts are selected for import. Keep currentStep at provider/search, refine the search, switch provider, or call select_promising_posts with usable posts before import_leads.");
1972
1975
  }
1973
1976
  // De-duplicate selected posts by canonical URL to avoid double scraping.
1974
1977
  const uniqueByUrl = new Map();
@@ -2016,19 +2019,15 @@ export async function importLeads(input) {
2016
2019
  message: `Started scraping ${uniqueSelectedPosts.length} posts (~${result.estimatedEngagers} engagers). Leads will appear as scraping completes.`,
2017
2020
  };
2018
2021
  }
2019
- // === APOLLO / SALES NAV FLOW ===
2022
+ // === SALES NAV / PROSPEO FLOW ===
2020
2023
  let leadListId = sourceLeadListId;
2021
2024
  let createdLeadList = null;
2022
2025
  // Create lead list if not provided
2023
2026
  if (!leadListId) {
2024
2027
  if (!searchId) {
2025
- throw new Error(`import_leads for ${provider || "apollo"} requires sourceLeadListId or searchId`);
2028
+ throw new Error(`import_leads for ${provider} requires sourceLeadListId or searchId`);
2026
2029
  }
2027
- const providerLabel = provider === "sales-nav"
2028
- ? "Sales Nav"
2029
- : provider === "prospeo"
2030
- ? "Prospeo"
2031
- : "Apollo";
2030
+ const providerLabel = provider === "sales-nav" ? "Sales Nav" : "Prospeo";
2032
2031
  const fallbackName = leadListName ||
2033
2032
  (searchName ? `${providerLabel} - ${searchName}` : undefined) ||
2034
2033
  `${providerLabel} Import ${new Date().toISOString().slice(0, 10)}`;
@@ -2052,6 +2051,7 @@ export async function importLeads(input) {
2052
2051
  return api.post(`/api/v3/sales-nav/export`, {
2053
2052
  searchId,
2054
2053
  workflowTableId: leadListId,
2054
+ campaignOfferId,
2055
2055
  targetLeadCount: cappedTargetLeadCount ?? 100,
2056
2056
  ...(normalizedMode ? { mode: normalizedMode } : {}),
2057
2057
  });
@@ -2059,16 +2059,12 @@ export async function importLeads(input) {
2059
2059
  if (provider === "prospeo") {
2060
2060
  return api.post(`/api/v3/lead-lists/${leadListId}/prospeo-import/start`, {
2061
2061
  searchId,
2062
+ campaignOfferId,
2062
2063
  targetLeadCount: cappedTargetLeadCount,
2063
2064
  ...(normalizedMode ? { mode: normalizedMode } : {}),
2064
2065
  });
2065
2066
  }
2066
- // Apollo import flow (default)
2067
- return api.post(`/api/v3/lead-lists/${leadListId}/apollo-import/start`, {
2068
- searchId,
2069
- targetLeadCount: cappedTargetLeadCount,
2070
- ...(normalizedMode ? { mode: normalizedMode } : {}),
2071
- });
2067
+ throw new Error(`Unsupported import provider ${provider}. Choose sales-nav, prospeo, or signal-discovery.`);
2072
2068
  };
2073
2069
  try {
2074
2070
  jobResult = await startImport();
@@ -2079,7 +2075,7 @@ export async function importLeads(input) {
2079
2075
  const parsed = parseApiErrorBody(error.body) || {};
2080
2076
  if (parsed.error === "lead_list_exists" || parsed.modeRequired) {
2081
2077
  return {
2082
- provider: provider || "apollo",
2078
+ provider,
2083
2079
  leadListId,
2084
2080
  existingLeadListId: leadListId,
2085
2081
  needsModeSelection: true,
@@ -2119,7 +2115,7 @@ export async function importLeads(input) {
2119
2115
  ...(shouldSetCurrentStep ? { currentStep: effectiveCurrentStep } : {}),
2120
2116
  });
2121
2117
  return {
2122
- provider: provider || "apollo",
2118
+ provider,
2123
2119
  leadListId,
2124
2120
  createdLeadList,
2125
2121
  jobResult,
@@ -2254,12 +2250,12 @@ export async function confirmLeadList(input) {
2254
2250
  }
2255
2251
  if (!readiness.ready) {
2256
2252
  if (readiness.reason === "missing_job_id") {
2257
- throw new Error("Import job ID is missing. Please provide the jobId for this import.");
2253
+ throw new Error("Import job ID is missing. Keep the campaign at confirm-lead-list; provide the jobId, retry readiness, cancel the import, or re-run-source before confirming.");
2258
2254
  }
2259
2255
  if (readiness.reason === "import_failed") {
2260
- throw new Error("Import failed. Please retry before confirming.");
2256
+ throw new Error("Import failed. Keep the campaign at confirm-lead-list; retry the provider import, cancel it, or re-run-source before confirming.");
2261
2257
  }
2262
- throw new Error("Import still in progress. Please wait for completion.");
2258
+ throw new Error("Import still in progress. Keep the campaign at confirm-lead-list; retry readiness, cancel the import, or re-run-source before launching post-import scouts.");
2263
2259
  }
2264
2260
  const importResult = await api.post(`/api/v3/campaign-builder/import-leads`, {
2265
2261
  sourceLeadListId: resolvedLeadListId,
@@ -9,15 +9,26 @@ export type CampaignOfferNavigation = {
9
9
  campaignBrief?: {
10
10
  content?: string | null;
11
11
  } | string | null;
12
+ useMessagingTemplate?: boolean | null;
13
+ messagingTemplateId?: string | null;
14
+ messagingTemplate?: unknown | null;
12
15
  leadSourceType?: string | null;
13
16
  leadSourceProvider?: string | null;
14
17
  selectedLeadListId?: string | null;
15
18
  workflowTableId?: string | null;
19
+ enableICPFilters?: boolean | null;
20
+ leadScoringRubrics?: Array<{
21
+ useCheckForScoring?: boolean | null;
22
+ }> | null;
23
+ senderIds?: string[] | null;
24
+ sequenceTemplate?: unknown | null;
25
+ campaignStatus?: string | null;
26
+ status?: string | null;
16
27
  currentStep?: string | null;
17
28
  };
18
29
  type NavigationDebugPayload = Record<string, unknown>;
19
30
  export declare function logNavigationDebug(event: string, payload?: NavigationDebugPayload, campaignId?: string | null): void;
20
- type CreateCampaignStepId = "campaign-created" | "pick-provider" | "provider-search" | "confirm-lead-list";
31
+ type CreateCampaignStepId = "campaign-created" | "pick-provider" | "provider-search" | "confirm-lead-list" | "filter-rules" | "messages" | "settings" | "sequence" | "send" | "running";
21
32
  export declare const navigationToolDefinitions: {
22
33
  name: string;
23
34
  description: string;
@@ -42,6 +53,10 @@ type NavigationComputeOptions = {
42
53
  tableChecked: boolean;
43
54
  rowCount: number | null;
44
55
  hasWorkflowRows: boolean | null;
56
+ selectedLeadListChecked?: boolean;
57
+ selectedLeadListAccessible?: boolean | null;
58
+ selectedLeadListRowCount?: number | null;
59
+ workflowTableAccessible?: boolean | null;
45
60
  initialWarnings?: string[];
46
61
  };
47
62
  export declare function computeCampaignNavigationStateFromCampaign(campaign: CampaignOfferNavigation, options: NavigationComputeOptions): {
@@ -63,6 +78,12 @@ export declare function computeCampaignNavigationStateFromCampaign(campaign: Cam
63
78
  leadSourceProvider: string | null;
64
79
  selectedLeadListId: string | null;
65
80
  providerCurrentStep: string | null;
81
+ enableICPFilters: boolean | null;
82
+ activeRubricCount: number;
83
+ hasApprovedMessageTemplate: boolean;
84
+ senderCount: number;
85
+ hasSequenceTemplate: boolean;
86
+ campaignStatus: string | null;
66
87
  };
67
88
  warnings: string[];
68
89
  };
@@ -85,6 +106,12 @@ export declare function getCampaignNavigationState(input: GetCampaignNavigationS
85
106
  leadSourceProvider: string | null;
86
107
  selectedLeadListId: string | null;
87
108
  providerCurrentStep: string | null;
109
+ enableICPFilters: boolean | null;
110
+ activeRubricCount: number;
111
+ hasApprovedMessageTemplate: boolean;
112
+ senderCount: number;
113
+ hasSequenceTemplate: boolean;
114
+ campaignStatus: string | null;
88
115
  };
89
116
  warnings: string[];
90
117
  }>;
@@ -134,6 +134,56 @@ function getBriefContent(brief) {
134
134
  return brief;
135
135
  return brief.content || "";
136
136
  }
137
+ function hasActiveRubrics(campaign) {
138
+ return (campaign.leadScoringRubrics?.some((rubric) => rubric.useCheckForScoring !== false) ?? false);
139
+ }
140
+ function hasApprovedMessageTemplate(campaign) {
141
+ if (campaign.messagingTemplateId || campaign.messagingTemplate)
142
+ return true;
143
+ const briefContent = getBriefContent(campaign.campaignBrief);
144
+ if (!briefContent.trim())
145
+ return false;
146
+ return (/approved message template/i.test(briefContent) &&
147
+ (/token fill rules/i.test(briefContent) ||
148
+ /token fill examples/i.test(briefContent) ||
149
+ /{{[^}]+}}/.test(briefContent)));
150
+ }
151
+ function hasSenderIds(campaign) {
152
+ return Array.isArray(campaign.senderIds) && campaign.senderIds.length > 0;
153
+ }
154
+ function hasSequenceTemplate(campaign) {
155
+ return Boolean(campaign.sequenceTemplate);
156
+ }
157
+ function isRunningCampaign(campaign) {
158
+ const status = String(campaign.campaignStatus ?? campaign.status ?? "")
159
+ .trim()
160
+ .toLowerCase();
161
+ return (campaign.currentStep === "running" ||
162
+ status === "running" ||
163
+ status === "active" ||
164
+ status === "started");
165
+ }
166
+ function shouldEvaluateTailState(campaign) {
167
+ const tailCurrentSteps = new Set([
168
+ "messages",
169
+ "auto-execute-leads",
170
+ "auto-execute-messaging",
171
+ "awaiting-user-greenlight",
172
+ "settings",
173
+ "sequence",
174
+ "send",
175
+ "claude-greenlight",
176
+ "running",
177
+ ]);
178
+ return ((campaign.currentStep
179
+ ? tailCurrentSteps.has(campaign.currentStep)
180
+ : false) ||
181
+ campaign.useMessagingTemplate === true ||
182
+ hasApprovedMessageTemplate(campaign) ||
183
+ hasSenderIds(campaign) ||
184
+ hasSequenceTemplate(campaign) ||
185
+ isRunningCampaign(campaign));
186
+ }
137
187
  function checkCampaignCreated(campaign) {
138
188
  const missing = [];
139
189
  if (!campaign.id)
@@ -161,36 +211,94 @@ function checkProviderSearch(campaign) {
161
211
  missing.push("leadSourceProvider");
162
212
  return { stepId: "provider-search", missing };
163
213
  }
164
- function checkConfirmLeadList(campaign, hasWorkflowRows, enforceRowCheck) {
214
+ function checkConfirmLeadList(campaign, hasWorkflowRows, enforceRowCheck, refs) {
165
215
  const missing = [];
166
216
  if (!campaign.selectedLeadListId)
167
217
  missing.push("selectedLeadListId");
168
218
  if (!campaign.workflowTableId)
169
219
  missing.push("workflowTableId");
220
+ if (campaign.selectedLeadListId &&
221
+ refs.selectedLeadListChecked &&
222
+ refs.selectedLeadListAccessible === false) {
223
+ missing.push("selectedLeadList");
224
+ }
225
+ if (campaign.workflowTableId && refs.workflowTableAccessible === false) {
226
+ missing.push("workflowTable");
227
+ }
170
228
  if (enforceRowCheck && hasWorkflowRows === false) {
171
229
  missing.push("workflowTableRows");
172
230
  }
173
231
  return { stepId: "confirm-lead-list", missing };
174
232
  }
233
+ function checkRubrics(campaign) {
234
+ const missing = [];
235
+ const filtersRequired = campaign.enableICPFilters === true;
236
+ if (filtersRequired && !hasActiveRubrics(campaign)) {
237
+ missing.push("leadScoringRubrics");
238
+ }
239
+ return { stepId: "filter-rules", missing };
240
+ }
241
+ function checkMessages(campaign) {
242
+ const missing = [];
243
+ if (!hasApprovedMessageTemplate(campaign)) {
244
+ missing.push("approvedMessageTemplate");
245
+ }
246
+ return { stepId: "messages", missing };
247
+ }
248
+ function checkSettings(campaign) {
249
+ const missing = [];
250
+ if (!hasSenderIds(campaign))
251
+ missing.push("senderIds");
252
+ return { stepId: "settings", missing };
253
+ }
254
+ function checkSequence(campaign) {
255
+ const missing = [];
256
+ if (!hasSequenceTemplate(campaign))
257
+ missing.push("sequenceTemplate");
258
+ return { stepId: "sequence", missing };
259
+ }
175
260
  function mapHeadlessToUiStep(step) {
176
261
  if (!step)
177
262
  return null;
178
- if (step === "configure-columns" || step === "confirm-lead-list") {
179
- return "leads";
180
- }
181
263
  if (step === "create-offer")
182
264
  return "plan";
265
+ if (step === "select-lead-source")
266
+ return "choose-source";
183
267
  if (step === "pick-provider")
184
268
  return "pick-provider";
185
269
  if (step === "apollo" || step === "sales-nav" || step === "prospeo") {
186
270
  return "contact-search";
187
271
  }
272
+ if (step === "configure-columns" ||
273
+ step === "apollo-select-leads" ||
274
+ step === "sales-nav-select-leads" ||
275
+ step === "prospeo-select-leads" ||
276
+ step === "signal-discovery-results" ||
277
+ step === "confirm-lead-list") {
278
+ return "leads";
279
+ }
188
280
  if (step === "signal-discovery")
189
281
  return "signal-discovery";
190
- if (step === "signal-discovery-results")
191
- return "leads";
192
282
  if (step === "filter-choice")
193
283
  return "filter-choice";
284
+ if (step === "create-icp-rubric")
285
+ return "filter-rules";
286
+ if (step === "apply-icp-rubric" || step === "validate-sample") {
287
+ return "filter-leads";
288
+ }
289
+ if (step === "messages" ||
290
+ step === "auto-execute-leads" ||
291
+ step === "auto-execute-messaging") {
292
+ return "messages";
293
+ }
294
+ if (step === "settings" || step === "awaiting-user-greenlight") {
295
+ return "settings";
296
+ }
297
+ if (step === "sequence")
298
+ return "sequence";
299
+ if (step === "send" || step === "claude-greenlight" || step === "running") {
300
+ return "send";
301
+ }
194
302
  return null;
195
303
  }
196
304
  export const navigationToolDefinitions = [
@@ -227,6 +335,11 @@ export function computeCampaignNavigationStateFromCampaign(campaign, options) {
227
335
  leadSourceProvider: campaign.leadSourceProvider ?? null,
228
336
  selectedLeadListId: campaign.selectedLeadListId ?? null,
229
337
  workflowTableId: campaign.workflowTableId ?? null,
338
+ enableICPFilters: campaign.enableICPFilters ?? null,
339
+ activeRubricCount: campaign.leadScoringRubrics?.filter((rubric) => rubric.useCheckForScoring !== false).length ?? 0,
340
+ senderCount: campaign.senderIds?.length ?? 0,
341
+ hasSequenceTemplate: hasSequenceTemplate(campaign),
342
+ campaignStatus: campaign.campaignStatus ?? campaign.status ?? null,
230
343
  currentStep: campaign.currentStep ?? null,
231
344
  },
232
345
  options: {
@@ -234,6 +347,9 @@ export function computeCampaignNavigationStateFromCampaign(campaign, options) {
234
347
  tableChecked: options.tableChecked,
235
348
  rowCount: options.rowCount,
236
349
  hasWorkflowRows: options.hasWorkflowRows,
350
+ selectedLeadListChecked: options.selectedLeadListChecked ?? false,
351
+ selectedLeadListAccessible: options.selectedLeadListAccessible ?? null,
352
+ workflowTableAccessible: options.workflowTableAccessible ?? null,
237
353
  initialWarningsCount: options.initialWarnings?.length ?? 0,
238
354
  },
239
355
  }, campaign.id);
@@ -252,8 +368,19 @@ export function computeCampaignNavigationStateFromCampaign(campaign, options) {
252
368
  checkCampaignCreated(campaign),
253
369
  checkPickProvider(campaign),
254
370
  checkProviderSearch(campaign),
255
- checkConfirmLeadList(campaign, options.hasWorkflowRows, enforceRowCheck),
371
+ checkConfirmLeadList(campaign, options.hasWorkflowRows, enforceRowCheck, {
372
+ selectedLeadListChecked: options.selectedLeadListChecked === true,
373
+ selectedLeadListAccessible: options.selectedLeadListAccessible ?? null,
374
+ workflowTableAccessible: options.workflowTableAccessible ?? null,
375
+ }),
256
376
  ];
377
+ if (campaign.enableICPFilters === true) {
378
+ checks.push(checkRubrics(campaign));
379
+ }
380
+ const evaluateTailState = shouldEvaluateTailState(campaign);
381
+ if (evaluateTailState) {
382
+ checks.push(checkMessages(campaign), checkSettings(campaign), checkSequence(campaign));
383
+ }
257
384
  let computedStep = "campaign-created";
258
385
  let blockedAt = null;
259
386
  let missing = [];
@@ -271,11 +398,24 @@ export function computeCampaignNavigationStateFromCampaign(campaign, options) {
271
398
  }
272
399
  computedStep = check.stepId;
273
400
  }
401
+ if (!blockedAt && evaluateTailState) {
402
+ computedStep = isRunningCampaign(campaign) ? "running" : "send";
403
+ }
274
404
  const expectedHeadlessStep = computedStep === "campaign-created"
275
405
  ? "create-offer"
276
406
  : computedStep === "provider-search"
277
407
  ? providerConfig?.currentStep || campaign.leadSourceProvider || null
278
- : computedStep;
408
+ : computedStep === "filter-rules"
409
+ ? "create-icp-rubric"
410
+ : computedStep === "messages"
411
+ ? "messages"
412
+ : computedStep === "settings"
413
+ ? "settings"
414
+ : computedStep === "sequence"
415
+ ? "sequence"
416
+ : computedStep === "running"
417
+ ? "running"
418
+ : computedStep;
279
419
  const stepAligned = !campaign.currentStep || !expectedHeadlessStep
280
420
  ? null
281
421
  : campaign.currentStep === expectedHeadlessStep;
@@ -304,6 +444,12 @@ export function computeCampaignNavigationStateFromCampaign(campaign, options) {
304
444
  leadSourceProvider: campaign.leadSourceProvider ?? null,
305
445
  selectedLeadListId: campaign.selectedLeadListId ?? null,
306
446
  providerCurrentStep: providerConfig?.currentStep ?? null,
447
+ enableICPFilters: campaign.enableICPFilters ?? null,
448
+ activeRubricCount: campaign.leadScoringRubrics?.filter((rubric) => rubric.useCheckForScoring !== false).length ?? 0,
449
+ hasApprovedMessageTemplate: hasApprovedMessageTemplate(campaign),
450
+ senderCount: campaign.senderIds?.length ?? 0,
451
+ hasSequenceTemplate: hasSequenceTemplate(campaign),
452
+ campaignStatus: campaign.campaignStatus ?? campaign.status ?? null,
307
453
  },
308
454
  warnings,
309
455
  };
@@ -328,7 +474,31 @@ export async function getCampaignNavigationState(input) {
328
474
  campaignId: input.campaignId,
329
475
  includeTableCheck,
330
476
  }, input.campaignId);
331
- const campaign = await api.get(`/api/v2/campaign-offers/${input.campaignId}`);
477
+ let campaign;
478
+ try {
479
+ campaign = await api.get(`/api/v2/campaign-offers/${input.campaignId}`);
480
+ }
481
+ catch (error) {
482
+ const message = error instanceof Error ? error.message : String(error);
483
+ const unavailable = computeCampaignNavigationStateFromCampaign({
484
+ id: input.campaignId,
485
+ campaignBrief: null,
486
+ currentStep: null,
487
+ }, {
488
+ includeTableCheck,
489
+ tableChecked: false,
490
+ rowCount: null,
491
+ hasWorkflowRows: null,
492
+ initialWarnings: [
493
+ `Campaign is missing or inaccessible. Remediation: confirm the campaignId and active workspace, then retry. Detail: ${message}`,
494
+ ],
495
+ });
496
+ return {
497
+ ...unavailable,
498
+ blockedAt: "campaign-created",
499
+ missing: ["campaign"],
500
+ };
501
+ }
332
502
  logNavigationDebug("state.fetch_campaign.success", {
333
503
  campaignId: input.campaignId,
334
504
  workflowTableId: campaign.workflowTableId ?? null,
@@ -338,8 +508,39 @@ export async function getCampaignNavigationState(input) {
338
508
  }, input.campaignId);
339
509
  let tableRowCount = null;
340
510
  let hasWorkflowRows = null;
511
+ let workflowTableAccessible = campaign.workflowTableId
512
+ ? null
513
+ : true;
514
+ let selectedLeadListChecked = false;
515
+ let selectedLeadListAccessible = campaign.selectedLeadListId
516
+ ? null
517
+ : true;
518
+ let selectedLeadListRowCount = null;
341
519
  const warnings = [];
342
520
  let tableChecked = false;
521
+ if (includeTableCheck &&
522
+ campaign.selectedLeadListId &&
523
+ campaign.selectedLeadListId !== campaign.workflowTableId) {
524
+ try {
525
+ const rows = await getTableRowsMinimal(campaign.selectedLeadListId, {
526
+ limit: 1,
527
+ page: 1,
528
+ });
529
+ selectedLeadListChecked = true;
530
+ selectedLeadListAccessible = true;
531
+ selectedLeadListRowCount = rows.pagination?.totalCount ?? 0;
532
+ }
533
+ catch (error) {
534
+ selectedLeadListChecked = true;
535
+ selectedLeadListAccessible = false;
536
+ warnings.push(`Selected source list is inaccessible or deleted. Remediation: re-select or recreate the lead list before resuming. Detail: ${error instanceof Error ? error.message : String(error)}`);
537
+ }
538
+ }
539
+ else if (campaign.selectedLeadListId) {
540
+ selectedLeadListChecked =
541
+ campaign.selectedLeadListId === campaign.workflowTableId;
542
+ selectedLeadListAccessible = null;
543
+ }
343
544
  if (includeTableCheck && campaign.workflowTableId) {
344
545
  logNavigationDebug("state.table_check.start", {
345
546
  campaignId: input.campaignId,
@@ -351,6 +552,7 @@ export async function getCampaignNavigationState(input) {
351
552
  page: 1,
352
553
  });
353
554
  tableChecked = true;
555
+ workflowTableAccessible = true;
354
556
  tableRowCount = rows.pagination?.totalCount ?? 0;
355
557
  hasWorkflowRows = tableRowCount > 0;
356
558
  logNavigationDebug("state.table_check.success", {
@@ -361,7 +563,11 @@ export async function getCampaignNavigationState(input) {
361
563
  }, input.campaignId);
362
564
  }
363
565
  catch (error) {
364
- warnings.push(`Failed to check workflow table rows: ${error instanceof Error ? error.message : String(error)}`);
566
+ tableChecked = true;
567
+ workflowTableAccessible = false;
568
+ tableRowCount = null;
569
+ hasWorkflowRows = null;
570
+ warnings.push(`Workflow table is inaccessible or deleted. Remediation: re-import confirmed leads before resuming. Detail: ${error instanceof Error ? error.message : String(error)}`);
365
571
  logNavigationDebug("state.table_check.error", {
366
572
  campaignId: input.campaignId,
367
573
  workflowTableId: campaign.workflowTableId,
@@ -374,6 +580,10 @@ export async function getCampaignNavigationState(input) {
374
580
  tableChecked,
375
581
  rowCount: tableRowCount,
376
582
  hasWorkflowRows,
583
+ selectedLeadListChecked,
584
+ selectedLeadListAccessible,
585
+ selectedLeadListRowCount,
586
+ workflowTableAccessible,
377
587
  initialWarnings: warnings,
378
588
  });
379
589
  logNavigationDebug("state.complete", {
@@ -283,7 +283,7 @@ export function getPostFindLeadsScoutRegistry() {
283
283
  joinGate: {
284
284
  afterAllComplete: true,
285
285
  requiredArtifacts: ["lead-filter.md", "message-validation.md"],
286
- show: ["lead_filter_summary", "sample_message"],
286
+ show: ["readable_filters_with_reasons", "sample_message_for_approval"],
287
287
  nextStage: "message-review",
288
288
  },
289
289
  usage: {
@@ -4,9 +4,8 @@ function scopedKey(provider, campaignOfferId) {
4
4
  }
5
5
  function resolveLoadedState(provider, campaignOfferId) {
6
6
  if (campaignOfferId) {
7
- const campaignScoped = providerPromptLoadedState.get(scopedKey(provider, campaignOfferId));
8
- if (campaignScoped)
9
- return campaignScoped;
7
+ return (providerPromptLoadedState.get(scopedKey(provider, campaignOfferId)) ??
8
+ null);
10
9
  }
11
10
  const providerScoped = providerPromptLoadedState.get(scopedKey(provider));
12
11
  return providerScoped ?? null;