@sellable/mcp 0.1.188 → 0.1.190

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 (29) hide show
  1. package/agents/post-find-leads-message-scout.md +8 -7
  2. package/agents/registry.json +2 -2
  3. package/dist/index-dev.js +0 -0
  4. package/dist/index.js +0 -0
  5. package/dist/server.js +22 -0
  6. package/dist/tools/campaign-processing.d.ts +383 -0
  7. package/dist/tools/campaign-processing.js +304 -0
  8. package/dist/tools/leads.d.ts +63 -19
  9. package/dist/tools/leads.js +59 -8
  10. package/dist/tools/prompts.js +2 -2
  11. package/dist/tools/registry.d.ts +289 -37
  12. package/dist/tools/registry.js +2 -0
  13. package/package.json +1 -1
  14. package/skills/create-campaign/SKILL.md +17 -12
  15. package/skills/create-campaign-v2/SKILL.md +23 -25
  16. package/skills/create-campaign-v2/SOUL.md +11 -4
  17. package/skills/create-campaign-v2/core/flow.v2.json +1 -1
  18. package/skills/create-campaign-v2/core/policy.md +3 -3
  19. package/skills/create-campaign-v2/references/approval-gate-framing.md +5 -5
  20. package/skills/create-campaign-v2/references/filter-leads.md +5 -3
  21. package/skills/create-campaign-v2/references/parallel-critique-protocol.md +2 -2
  22. package/skills/create-campaign-v2/references/sample-validation-loop.md +46 -66
  23. package/skills/create-campaign-v2/references/step-15-re-cascade.md +20 -13
  24. package/skills/create-campaign-v2-tail/SKILL.md +52 -65
  25. package/skills/find-leads/SKILL.md +2 -1
  26. package/skills/generate-messages/SKILL.md +11 -0
  27. package/skills/generate-messages-compact/SKILL.md +100 -0
  28. package/skills/generate-messages-compact/references/examples-critique-revision.md +37 -0
  29. package/skills/providers/signal-discovery.md +9 -3
@@ -0,0 +1,304 @@
1
+ import { getApi } from "../api.js";
2
+ import { resolveWaitTimeout } from "./readiness.js";
3
+ const DEFAULT_TIMEOUT_MS = 90000;
4
+ const DEFAULT_INTERVAL_MS = 2000;
5
+ function sleep(ms) {
6
+ return new Promise((resolve) => setTimeout(resolve, ms));
7
+ }
8
+ function stableHash(value) {
9
+ const text = JSON.stringify(value, Object.keys(value).sort());
10
+ let hash = 2166136261;
11
+ for (let index = 0; index < text.length; index += 1) {
12
+ hash ^= text.charCodeAt(index);
13
+ hash = Math.imul(hash, 16777619);
14
+ }
15
+ return (hash >>> 0).toString(16).padStart(8, "0");
16
+ }
17
+ async function postCampaignProcessing(body) {
18
+ const api = getApi();
19
+ return api.post("/api/v3/mcp/campaign-processing", body);
20
+ }
21
+ async function resolveSchema(input) {
22
+ return postCampaignProcessing({
23
+ action: "schema",
24
+ campaignId: input.campaignId,
25
+ tableId: input.tableId,
26
+ });
27
+ }
28
+ export const campaignProcessingToolDefinitions = [
29
+ {
30
+ name: "get_campaign_table_schema",
31
+ description: "Return compact semantic campaign-table schema: column roles, row count, review-batch metadata, and current approved brief hash. Use for debugging or recovery, not normal queueing.",
32
+ inputSchema: {
33
+ type: "object",
34
+ properties: {
35
+ campaignId: { type: "string" },
36
+ tableId: { type: "string" },
37
+ },
38
+ additionalProperties: false,
39
+ },
40
+ },
41
+ {
42
+ name: "select_campaign_cells",
43
+ description: "Dry-run semantic campaign-cell selection. Returns compact counts and a tiny sample, never bulky row data. Use before queue_campaign_cells only when debugging or explaining a recovery action.",
44
+ inputSchema: {
45
+ type: "object",
46
+ properties: {
47
+ campaignId: { type: "string" },
48
+ tableId: { type: "string" },
49
+ columnRole: {
50
+ type: "string",
51
+ enum: [
52
+ "enrich",
53
+ "passesRubric",
54
+ "icpScore",
55
+ "generateMessage",
56
+ "approved",
57
+ ],
58
+ },
59
+ rowSelector: {
60
+ type: "object",
61
+ properties: {
62
+ type: {
63
+ type: "string",
64
+ enum: [
65
+ "reviewBatch",
66
+ "rowIds",
67
+ "passedRows",
68
+ "needsGeneratedMessage",
69
+ "staleGeneratedMessages",
70
+ ],
71
+ },
72
+ basisHash: { type: "string" },
73
+ rowIds: { type: "array", items: { type: "string" } },
74
+ limit: { type: "number" },
75
+ },
76
+ required: ["type"],
77
+ additionalProperties: false,
78
+ },
79
+ limit: { type: "number" },
80
+ },
81
+ required: ["columnRole", "rowSelector"],
82
+ additionalProperties: false,
83
+ },
84
+ },
85
+ {
86
+ name: "queue_campaign_cells",
87
+ 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. Use forceRerun:true for message-template revisions.",
88
+ inputSchema: {
89
+ type: "object",
90
+ properties: {
91
+ campaignId: { type: "string" },
92
+ tableId: { type: "string" },
93
+ columnRole: {
94
+ type: "string",
95
+ enum: ["enrich", "generateMessage", "approved"],
96
+ },
97
+ rowSelector: {
98
+ type: "object",
99
+ properties: {
100
+ type: {
101
+ type: "string",
102
+ enum: [
103
+ "reviewBatch",
104
+ "rowIds",
105
+ "passedRows",
106
+ "needsGeneratedMessage",
107
+ "staleGeneratedMessages",
108
+ ],
109
+ },
110
+ basisHash: { type: "string" },
111
+ rowIds: { type: "array", items: { type: "string" } },
112
+ limit: { type: "number" },
113
+ },
114
+ required: ["type"],
115
+ additionalProperties: false,
116
+ },
117
+ forceRerun: { type: "boolean" },
118
+ limit: { type: "number" },
119
+ reason: { type: "string" },
120
+ },
121
+ required: ["columnRole", "rowSelector"],
122
+ additionalProperties: false,
123
+ },
124
+ },
125
+ {
126
+ name: "wait_for_campaign_processing",
127
+ description: "Stats-only campaign processing wait. Use for fit/message readiness instead of wait_for_rubric_results when waiting on campaign-table processing. Generated messages are counted separately from rubric passes; current template revision is enforced through messageDraftBasis.approvedBriefHash.",
128
+ inputSchema: {
129
+ type: "object",
130
+ properties: {
131
+ campaignId: { type: "string" },
132
+ tableId: { type: "string" },
133
+ minPassedCount: { type: "number" },
134
+ minGeneratedMessages: { type: "number" },
135
+ templateRevision: {
136
+ type: "string",
137
+ description: '"current" (default when minGeneratedMessages is set) or an explicit current approved brief hash.',
138
+ },
139
+ timeoutMs: { type: "number" },
140
+ intervalMs: { type: "number" },
141
+ },
142
+ additionalProperties: false,
143
+ },
144
+ },
145
+ {
146
+ name: "revise_message_template_and_rerun",
147
+ description: "Update the approved message template in the campaign brief, mark prior generated messages stale, reset unsent approvals, and force-rerun Generate Message cells. Does not directly overwrite row message cells.",
148
+ inputSchema: {
149
+ type: "object",
150
+ properties: {
151
+ campaignId: { type: "string" },
152
+ tableId: { type: "string" },
153
+ templateMarkdown: { type: "string" },
154
+ approvedMessageTemplate: { type: "string" },
155
+ rowSelector: {
156
+ type: "object",
157
+ properties: {
158
+ type: {
159
+ type: "string",
160
+ enum: [
161
+ "passedRows",
162
+ "needsGeneratedMessage",
163
+ "staleGeneratedMessages",
164
+ "rowIds",
165
+ ],
166
+ },
167
+ rowIds: { type: "array", items: { type: "string" } },
168
+ limit: { type: "number" },
169
+ },
170
+ required: ["type"],
171
+ additionalProperties: false,
172
+ },
173
+ limit: { type: "number" },
174
+ },
175
+ required: ["campaignId"],
176
+ additionalProperties: false,
177
+ },
178
+ },
179
+ ];
180
+ export async function getCampaignTableSchema(input) {
181
+ return resolveSchema(input);
182
+ }
183
+ export async function selectCampaignCells(input) {
184
+ return postCampaignProcessing({
185
+ action: "select",
186
+ ...input,
187
+ });
188
+ }
189
+ export async function queueCampaignCells(input) {
190
+ return postCampaignProcessing({
191
+ action: "queue",
192
+ ...input,
193
+ });
194
+ }
195
+ export async function reviseMessageTemplateAndRerun(input) {
196
+ return postCampaignProcessing({
197
+ action: "reviseTemplateAndRerun",
198
+ ...input,
199
+ });
200
+ }
201
+ export async function waitForCampaignProcessing(input) {
202
+ const api = getApi();
203
+ const schema = await resolveSchema(input);
204
+ const { effectiveTimeoutMs, guardApplied, requestedTimeoutMs } = resolveWaitTimeout(input.timeoutMs ?? DEFAULT_TIMEOUT_MS);
205
+ const intervalMs = Math.max(500, input.intervalMs ?? DEFAULT_INTERVAL_MS);
206
+ const minPassedCount = typeof input.minPassedCount === "number" &&
207
+ Number.isFinite(input.minPassedCount)
208
+ ? Math.max(1, Math.floor(input.minPassedCount))
209
+ : null;
210
+ const minGeneratedMessages = typeof input.minGeneratedMessages === "number" &&
211
+ Number.isFinite(input.minGeneratedMessages)
212
+ ? Math.max(1, Math.floor(input.minGeneratedMessages))
213
+ : null;
214
+ const templateRevision = input.templateRevision ?? (minGeneratedMessages !== null ? "current" : null);
215
+ const pollKey = stableHash({
216
+ tableId: schema.tableId,
217
+ minPassedCount,
218
+ minGeneratedMessages,
219
+ templateRevision,
220
+ });
221
+ const start = Date.now();
222
+ let attempts = 0;
223
+ let lastStats = null;
224
+ while (Date.now() - start <= effectiveTimeoutMs) {
225
+ attempts += 1;
226
+ const stats = await api.get(`/api/v3/workflow-tables/${schema.tableId}/stats`);
227
+ lastStats = stats;
228
+ const currentRevisionHash = stats.currentTemplateRevisionHash ?? null;
229
+ if (templateRevision &&
230
+ templateRevision !== "current" &&
231
+ currentRevisionHash &&
232
+ templateRevision !== currentRevisionHash) {
233
+ return {
234
+ ready: false,
235
+ reason: "template_revision_mismatch",
236
+ attempts,
237
+ elapsedMs: Date.now() - start,
238
+ tableId: schema.tableId,
239
+ pollKey,
240
+ requestedTemplateRevision: templateRevision,
241
+ currentTemplateRevisionHash: currentRevisionHash,
242
+ guidance: "The requested templateRevision is not current. Re-read campaign state or wait using templateRevision:'current'.",
243
+ stats,
244
+ };
245
+ }
246
+ const passed = stats.passRate?.passed ?? 0;
247
+ const generated = templateRevision === "current" || templateRevision !== null
248
+ ? (stats.currentRevisionGeneratedMessagesCount ?? 0)
249
+ : (stats.generatedMessagesCount ?? stats.messagesCount ?? 0);
250
+ const passFloorMet = minPassedCount === null || passed >= minPassedCount;
251
+ const generatedFloorMet = minGeneratedMessages === null || generated >= minGeneratedMessages;
252
+ if (passFloorMet && generatedFloorMet) {
253
+ return {
254
+ ready: true,
255
+ attempts,
256
+ elapsedMs: Date.now() - start,
257
+ tableId: schema.tableId,
258
+ pollKey,
259
+ floors: {
260
+ minPassedCount,
261
+ minGeneratedMessages,
262
+ templateRevision,
263
+ },
264
+ result: {
265
+ passedCount: passed,
266
+ generatedMessagesCount: stats.generatedMessagesCount ?? stats.messagesCount ?? 0,
267
+ currentRevisionGeneratedMessagesCount: stats.currentRevisionGeneratedMessagesCount ?? 0,
268
+ staleGeneratedMessagesCount: stats.staleGeneratedMessagesCount ?? 0,
269
+ generatedWithoutPassCount: stats.generatedWithoutPassCount ?? 0,
270
+ currentTemplateRevisionHash: currentRevisionHash,
271
+ },
272
+ stats,
273
+ };
274
+ }
275
+ await sleep(intervalMs);
276
+ }
277
+ const passed = lastStats?.passRate?.passed ?? 0;
278
+ return {
279
+ ready: false,
280
+ reason: guardApplied ? "tool_timeout_guard" : "timeout",
281
+ attempts,
282
+ elapsedMs: Date.now() - start,
283
+ tableId: schema.tableId,
284
+ pollKey,
285
+ warning: guardApplied
286
+ ? `Stopped after ${effectiveTimeoutMs}ms to avoid host tool timeout (requested ${requestedTimeoutMs}ms).`
287
+ : undefined,
288
+ partialResult: {
289
+ passedCount: passed,
290
+ generatedMessagesCount: lastStats?.generatedMessagesCount ?? lastStats?.messagesCount ?? 0,
291
+ currentRevisionGeneratedMessagesCount: lastStats?.currentRevisionGeneratedMessagesCount ?? 0,
292
+ staleGeneratedMessagesCount: lastStats?.staleGeneratedMessagesCount ?? 0,
293
+ generatedWithoutPassCount: lastStats?.generatedWithoutPassCount ?? 0,
294
+ currentTemplateRevisionHash: lastStats?.currentTemplateRevisionHash ??
295
+ schema.currentApprovedBriefHash,
296
+ passFloorMet: minPassedCount === null || passed >= minPassedCount,
297
+ generatedFloorMet: minGeneratedMessages === null ||
298
+ (lastStats?.currentRevisionGeneratedMessagesCount ?? 0) >=
299
+ minGeneratedMessages,
300
+ },
301
+ guidance: "Do not repoll with identical arguments after this timeout. Use the partial diagnostics to revise filters/messages, queue missing work, or surface the blocked sample state.",
302
+ stats: lastStats,
303
+ };
304
+ }
@@ -146,6 +146,7 @@ export type ConfirmLeadListInput = {
146
146
  keepInSync?: boolean;
147
147
  jobId?: string;
148
148
  reviewBatchLimit?: number;
149
+ includeRawImportResult?: boolean;
149
150
  allowPartialSourceList?: boolean;
150
151
  /**
151
152
  * Deprecated alias for reviewBatchLimit. Confirming a lead list now copies the
@@ -246,6 +247,7 @@ export declare const leadToolDefinitions: ({
246
247
  jobId?: undefined;
247
248
  reviewBatchLimit?: undefined;
248
249
  allowPartialSourceList?: undefined;
250
+ includeRawImportResult?: undefined;
249
251
  selections?: undefined;
250
252
  selectionMode?: undefined;
251
253
  };
@@ -423,6 +425,7 @@ export declare const leadToolDefinitions: ({
423
425
  jobId?: undefined;
424
426
  reviewBatchLimit?: undefined;
425
427
  allowPartialSourceList?: undefined;
428
+ includeRawImportResult?: undefined;
426
429
  selections?: undefined;
427
430
  selectionMode?: undefined;
428
431
  };
@@ -499,6 +502,7 @@ export declare const leadToolDefinitions: ({
499
502
  jobId?: undefined;
500
503
  reviewBatchLimit?: undefined;
501
504
  allowPartialSourceList?: undefined;
505
+ includeRawImportResult?: undefined;
502
506
  selections?: undefined;
503
507
  selectionMode?: undefined;
504
508
  };
@@ -647,6 +651,7 @@ export declare const leadToolDefinitions: ({
647
651
  jobId?: undefined;
648
652
  reviewBatchLimit?: undefined;
649
653
  allowPartialSourceList?: undefined;
654
+ includeRawImportResult?: undefined;
650
655
  selections?: undefined;
651
656
  selectionMode?: undefined;
652
657
  };
@@ -737,6 +742,7 @@ export declare const leadToolDefinitions: ({
737
742
  jobId?: undefined;
738
743
  reviewBatchLimit?: undefined;
739
744
  allowPartialSourceList?: undefined;
745
+ includeRawImportResult?: undefined;
740
746
  selections?: undefined;
741
747
  selectionMode?: undefined;
742
748
  };
@@ -836,6 +842,7 @@ export declare const leadToolDefinitions: ({
836
842
  jobId?: undefined;
837
843
  reviewBatchLimit?: undefined;
838
844
  allowPartialSourceList?: undefined;
845
+ includeRawImportResult?: undefined;
839
846
  selections?: undefined;
840
847
  selectionMode?: undefined;
841
848
  };
@@ -917,6 +924,7 @@ export declare const leadToolDefinitions: ({
917
924
  jobId?: undefined;
918
925
  reviewBatchLimit?: undefined;
919
926
  allowPartialSourceList?: undefined;
927
+ includeRawImportResult?: undefined;
920
928
  selections?: undefined;
921
929
  selectionMode?: undefined;
922
930
  };
@@ -1537,6 +1545,7 @@ export declare const leadToolDefinitions: ({
1537
1545
  jobId?: undefined;
1538
1546
  reviewBatchLimit?: undefined;
1539
1547
  allowPartialSourceList?: undefined;
1548
+ includeRawImportResult?: undefined;
1540
1549
  selections?: undefined;
1541
1550
  selectionMode?: undefined;
1542
1551
  };
@@ -1670,6 +1679,7 @@ export declare const leadToolDefinitions: ({
1670
1679
  jobId?: undefined;
1671
1680
  reviewBatchLimit?: undefined;
1672
1681
  allowPartialSourceList?: undefined;
1682
+ includeRawImportResult?: undefined;
1673
1683
  selections?: undefined;
1674
1684
  selectionMode?: undefined;
1675
1685
  };
@@ -1791,6 +1801,7 @@ export declare const leadToolDefinitions: ({
1791
1801
  jobId?: undefined;
1792
1802
  reviewBatchLimit?: undefined;
1793
1803
  allowPartialSourceList?: undefined;
1804
+ includeRawImportResult?: undefined;
1794
1805
  selections?: undefined;
1795
1806
  selectionMode?: undefined;
1796
1807
  };
@@ -1870,6 +1881,7 @@ export declare const leadToolDefinitions: ({
1870
1881
  jobId?: undefined;
1871
1882
  reviewBatchLimit?: undefined;
1872
1883
  allowPartialSourceList?: undefined;
1884
+ includeRawImportResult?: undefined;
1873
1885
  selections?: undefined;
1874
1886
  selectionMode?: undefined;
1875
1887
  };
@@ -1921,6 +1933,10 @@ export declare const leadToolDefinitions: ({
1921
1933
  type: string;
1922
1934
  description: string;
1923
1935
  };
1936
+ includeRawImportResult: {
1937
+ type: string;
1938
+ description: string;
1939
+ };
1924
1940
  provider?: undefined;
1925
1941
  searchMode?: undefined;
1926
1942
  organization_num_employees_ranges?: undefined;
@@ -2083,6 +2099,7 @@ export declare const leadToolDefinitions: ({
2083
2099
  jobId?: undefined;
2084
2100
  reviewBatchLimit?: undefined;
2085
2101
  allowPartialSourceList?: undefined;
2102
+ includeRawImportResult?: undefined;
2086
2103
  };
2087
2104
  required: string[];
2088
2105
  };
@@ -2164,6 +2181,7 @@ export declare const leadToolDefinitions: ({
2164
2181
  jobId?: undefined;
2165
2182
  reviewBatchLimit?: undefined;
2166
2183
  allowPartialSourceList?: undefined;
2184
+ includeRawImportResult?: undefined;
2167
2185
  selections?: undefined;
2168
2186
  selectionMode?: undefined;
2169
2187
  };
@@ -2608,25 +2626,13 @@ export declare function cancelLeadImport(input: CancelLeadImportInput): Promise<
2608
2626
  previousStatus?: undefined;
2609
2627
  }>;
2610
2628
  export declare function confirmLeadList(input: ConfirmLeadListInput): Promise<{
2611
- sourceLeadListId: string;
2612
- campaignTableId: string | null;
2613
- importResult: {
2614
- workflowTableId?: string;
2615
- campaignTableId?: string;
2616
- campaignTableName?: string;
2617
- leadsImported?: number;
2618
- leadsSkipped?: number;
2619
- rowIds?: string[];
2620
- requestedTargetLeadCount?: number;
2621
- sourceRowLimit?: number;
2622
- async?: boolean;
2623
- hybrid?: boolean;
2624
- campaignTableReady?: boolean;
2625
- importStatus?: string | null;
2626
- remainingRowCount?: number | null;
2627
- rowCount?: number | null;
2628
- sourceRowCount?: number | null;
2629
- clonedSourceRowCount?: number | null;
2629
+ reviewBatch: {
2630
+ tableId: string | null;
2631
+ rowCount: number;
2632
+ rowIds: string[];
2633
+ enrichCellIds: string[];
2634
+ basisHash: string | null;
2635
+ recordedAt: string | null;
2630
2636
  };
2631
2637
  messageDraftBuilder: {
2632
2638
  firstAllowedStartPoint: string;
@@ -2637,6 +2643,7 @@ export declare function confirmLeadList(input: ConfirmLeadListInput): Promise<{
2637
2643
  selectedLeadListId: string;
2638
2644
  workflowTableId: string | null;
2639
2645
  reviewBatchRowIds: string[];
2646
+ reviewBatchBasisHash: string | null;
2640
2647
  reviewBatchRowCount: number;
2641
2648
  copiedCampaignRowCount: number;
2642
2649
  };
@@ -2665,6 +2672,43 @@ export declare function confirmLeadList(input: ConfirmLeadListInput): Promise<{
2665
2672
  warning: string | null;
2666
2673
  };
2667
2674
  message: string;
2675
+ importResult?: {
2676
+ workflowTableId?: string;
2677
+ campaignTableId?: string;
2678
+ campaignTableName?: string;
2679
+ leadsImported?: number;
2680
+ leadsSkipped?: number;
2681
+ rowIds?: string[];
2682
+ requestedTargetLeadCount?: number;
2683
+ sourceRowLimit?: number;
2684
+ async?: boolean;
2685
+ hybrid?: boolean;
2686
+ campaignTableReady?: boolean;
2687
+ importStatus?: string | null;
2688
+ remainingRowCount?: number | null;
2689
+ rowCount?: number | null;
2690
+ sourceRowCount?: number | null;
2691
+ clonedSourceRowCount?: number | null;
2692
+ } | undefined;
2693
+ sourceLeadListId: string;
2694
+ campaignTableId: string | null;
2695
+ importSummary: {
2696
+ workflowTableId: string | null;
2697
+ campaignTableId: string | null;
2698
+ campaignTableName: string | null;
2699
+ leadsImported: number | null;
2700
+ leadsSkipped: number | null;
2701
+ rowCount: number;
2702
+ requestedTargetLeadCount: number | null;
2703
+ sourceRowLimit: number | null;
2704
+ async: boolean;
2705
+ hybrid: boolean;
2706
+ campaignTableReady: true;
2707
+ importStatus: string | null;
2708
+ remainingRowCount: number;
2709
+ sourceRowCount: number | null;
2710
+ clonedSourceRowCount: number | null;
2711
+ };
2668
2712
  }>;
2669
2713
  export declare function getProviderPrompt(input: GetProviderPromptInput): string;
2670
2714
  export declare function selectPromisingPosts(input: SelectPromisingPostsInput): Promise<{
@@ -529,7 +529,7 @@ function buildSourceImportWatchNarration({ provider, selectedPostCount, estimate
529
529
  : "Apollo";
530
530
  const sourceDetail = provider === "signal-discovery"
531
531
  ? `${selectedPostCount ?? "selected"} approved post${selectedPostCount === 1 ? "" : "s"}${typeof estimatedEngagers === "number"
532
- ? ` with about ${estimatedEngagers.toLocaleString("en-US")} people to check`
532
+ ? ` with up to ${estimatedEngagers.toLocaleString("en-US")} people to check before top-level scrape adjustment`
533
533
  : ""}`
534
534
  : `the approved ${providerLabel} source`;
535
535
  const targetDetail = typeof targetLeadCount === "number"
@@ -806,8 +806,8 @@ function buildSignalDiscoverySourceRecommendation({ selectedPosts, targetEngager
806
806
  ? "the approved buyer-search target"
807
807
  : `the ${Math.round(defaultFitRate * 100)}% starting estimate`;
808
808
  const selectedPoolCopy = recommendedCount < selectedCount
809
- ? `**Posts considered:** ${selectedCount.toLocaleString("en-US")} selected posts with ${formatApproxInteger(totalVisibleEngagement)} public reactions/comments<br>\n**Recommended first set:** ${recommendedCount.toLocaleString("en-US")} post${recommendedCount === 1 ? "" : "s"} with ${formatApproxInteger(recommendedVisibleEngagement)} public reactions/comments and up to ${formatApproxInteger(scrapePlan.estimatedEngagers)} people to check<br>`
810
- : `**Public activity in the selected posts:** ${formatApproxInteger(totalVisibleEngagement)} reactions/comments<br>\n**People we can check from this set:** up to ${formatApproxInteger(scrapePlan.estimatedEngagers)}<br>`;
809
+ ? `**Posts considered:** ${selectedCount.toLocaleString("en-US")} selected posts with ${formatApproxInteger(totalVisibleEngagement)} visible public reactions/comments<br>\n**Recommended first set:** ${recommendedCount.toLocaleString("en-US")} post${recommendedCount === 1 ? "" : "s"} with ${formatApproxInteger(recommendedVisibleEngagement)} visible public reactions/comments and up to ${formatApproxInteger(scrapePlan.estimatedEngagers)} people to check before top-level comment and dedupe adjustment<br>`
810
+ : `**Visible public activity in the selected posts:** ${formatApproxInteger(totalVisibleEngagement)} reactions/comments<br>\n**People we can check from this set:** up to ${formatApproxInteger(scrapePlan.estimatedEngagers)} before top-level comment and dedupe adjustment<br>`;
811
811
  const message = `## Source Recommendation
812
812
 
813
813
  Start with LinkedIn posts.
@@ -821,7 +821,7 @@ I found ${recommendedCount.toLocaleString("en-US")} recommended LinkedIn post${r
821
821
 
822
822
  ### Recommended posts
823
823
 
824
- | Post | Why this post | Public activity | People we can check |
824
+ | Post | Why this post | Visible public activity | People we can check |
825
825
  |---|---|---:|---:|
826
826
  ${tableRows || "| Recommended posts | Campaign-matched public activity | - | - |"}
827
827
 
@@ -1518,6 +1518,10 @@ export const leadToolDefinitions = [
1518
1518
  type: "boolean",
1519
1519
  description: "Set true after user approval when interaction mode requires confirmation for confirm/import.",
1520
1520
  },
1521
+ includeRawImportResult: {
1522
+ type: "boolean",
1523
+ description: "Debug only. When true, include the raw import response; default compact output omits large row-id payloads.",
1524
+ },
1521
1525
  },
1522
1526
  required: ["campaignOfferId"],
1523
1527
  },
@@ -2579,7 +2583,7 @@ export async function importLeads(input) {
2579
2583
  maxPostsToScrape: normalizePositiveInteger(maxPostsToScrape) ?? null,
2580
2584
  limitedSelectedPosts: importSelection.limited,
2581
2585
  targetLeadCount: cappedTargetLeadCount ?? null,
2582
- message: `Started scraping ${postsToScrape.length} posts (~${result.estimatedEngagers} people to check). Leads will appear as scraping completes.${importSelection.limited
2586
+ message: `Started scraping ${postsToScrape.length} posts (up to ~${result.estimatedEngagers} people to check before top-level scrape adjustment). Leads will appear as scraping completes.${importSelection.limited
2583
2587
  ? ` Limited from ${uniqueSelectedPosts.length} selected posts by the approved source-capacity scrape plan.`
2584
2588
  : ""} The watched campaign has been moved to confirm-lead-list with import progress copy. Keep calling wait_for_lead_list_ready until it reports ready, failed, or cancelled before confirm_lead_list; only confirm a partial list with allowPartialSourceList after the user explicitly asks to keep going early. Do not call update_campaign to fix that step.`,
2585
2589
  };
@@ -2741,7 +2745,7 @@ export async function cancelLeadImport(input) {
2741
2745
  }
2742
2746
  export async function confirmLeadList(input) {
2743
2747
  const api = getApi();
2744
- const { campaignOfferId, currentStep, confirmed, sourceLeadListId, campaignName, keepInSync, jobId, reviewBatchLimit, allowPartialSourceList, targetLeadCount, } = input;
2748
+ const { campaignOfferId, currentStep, confirmed, sourceLeadListId, campaignName, keepInSync, jobId, reviewBatchLimit, includeRawImportResult, allowPartialSourceList, targetLeadCount, } = input;
2745
2749
  assertInteractionApproval({
2746
2750
  campaignId: campaignOfferId,
2747
2751
  action: "confirm-lead-list",
@@ -2988,6 +2992,50 @@ export async function confirmLeadList(input) {
2988
2992
  reviewSampleRows = [];
2989
2993
  }
2990
2994
  }
2995
+ const sampleReviewRowIds = reviewSampleRows
2996
+ .map((row) => row.id)
2997
+ .filter((rowId) => typeof rowId === "string");
2998
+ const reviewBatchRowIds = importedRowIds.length > 0
2999
+ ? importedRowIds.slice(0, keptReviewRowCount)
3000
+ : sampleReviewRowIds.slice(0, keptReviewRowCount);
3001
+ const reviewBatchEnrichCellIds = reviewSampleRows
3002
+ .map((row) => row.enrichCellId)
3003
+ .filter((cellId) => typeof cellId === "string");
3004
+ let recordedReviewBatch = null;
3005
+ if (campaignTableId && reviewBatchRowIds.length > 0) {
3006
+ const recordResult = await api.post("/api/v3/mcp/campaign-processing", {
3007
+ action: "recordReviewBatch",
3008
+ tableId: campaignTableId,
3009
+ rowIds: reviewBatchRowIds,
3010
+ enrichCellIds: reviewBatchEnrichCellIds,
3011
+ });
3012
+ recordedReviewBatch = recordResult.reviewBatch ?? null;
3013
+ }
3014
+ const compactReviewBatch = {
3015
+ tableId: campaignTableId ?? null,
3016
+ rowCount: recordedReviewBatch?.rowCount ?? reviewBatchRowIds.length,
3017
+ rowIds: recordedReviewBatch?.rowIds ?? reviewBatchRowIds,
3018
+ enrichCellIds: recordedReviewBatch?.enrichCellIds ?? reviewBatchEnrichCellIds,
3019
+ basisHash: recordedReviewBatch?.basisHash ?? null,
3020
+ recordedAt: recordedReviewBatch?.recordedAt ?? null,
3021
+ };
3022
+ const importSummary = {
3023
+ workflowTableId: campaignTableId ?? null,
3024
+ campaignTableId: campaignTableId ?? null,
3025
+ campaignTableName: importResult.campaignTableName ?? null,
3026
+ leadsImported: importResult.leadsImported ?? null,
3027
+ leadsSkipped: importResult.leadsSkipped ?? null,
3028
+ rowCount: importResult.rowCount ?? importedRowCount,
3029
+ requestedTargetLeadCount: importResult.requestedTargetLeadCount ?? null,
3030
+ sourceRowLimit: importResult.sourceRowLimit ?? null,
3031
+ async: importResult.async ?? false,
3032
+ hybrid: importResult.hybrid ?? false,
3033
+ campaignTableReady,
3034
+ importStatus: importResult.importStatus ?? null,
3035
+ remainingRowCount,
3036
+ sourceRowCount: importResult.sourceRowCount ?? null,
3037
+ clonedSourceRowCount: importResult.clonedSourceRowCount ?? null,
3038
+ };
2991
3039
  // Persist currentStep if the caller asked for it. Do NOT touch
2992
3040
  // selectedLeadListId here: the campaign table id is already saved to
2993
3041
  // CampaignOffer.workflowTableId by /api/v3/campaign-builder/import-leads,
@@ -3014,7 +3062,9 @@ export async function confirmLeadList(input) {
3014
3062
  return {
3015
3063
  sourceLeadListId: resolvedLeadListId,
3016
3064
  campaignTableId: campaignTableId ?? null,
3017
- importResult,
3065
+ importSummary,
3066
+ ...(includeRawImportResult ? { importResult } : {}),
3067
+ reviewBatch: compactReviewBatch,
3018
3068
  messageDraftBuilder: {
3019
3069
  firstAllowedStartPoint: "confirm_lead_list",
3020
3070
  startAllowed: true,
@@ -3023,7 +3073,8 @@ export async function confirmLeadList(input) {
3023
3073
  campaignOfferId,
3024
3074
  selectedLeadListId: resolvedLeadListId,
3025
3075
  workflowTableId: campaignTableId ?? null,
3026
- reviewBatchRowIds: importedRowIds.slice(0, keptReviewRowCount),
3076
+ reviewBatchRowIds,
3077
+ reviewBatchBasisHash: compactReviewBatch.basisHash,
3027
3078
  reviewBatchRowCount: keptReviewRowCount,
3028
3079
  copiedCampaignRowCount: importedRowCount,
3029
3080
  },
@@ -315,7 +315,7 @@ export function getPostFindLeadsScoutRegistry() {
315
315
  "basis.workflowTableId",
316
316
  "basis.reviewSampleRowHash or basis.reviewSampleRowIds",
317
317
  ],
318
- promptRequired: 'get_subskill_prompt({ subskillName: "generate-messages", offset, limit }) until hasMore=false',
318
+ promptRequired: 'get_subskill_prompt({ subskillName: "generate-messages-compact" }); load generate-messages-compact/references/examples-critique-revision.md only for critique, revision, or close-call examples',
319
319
  basisFields: [
320
320
  "campaign revision or updatedAt",
321
321
  "brief hash",
@@ -342,7 +342,7 @@ export function getPostFindLeadsScoutRegistry() {
342
342
  usage: {
343
343
  codex: "After confirm_lead_list copies source rows and the initial campaign-table execution slice exists, ask the filter-choice question immediately. Do not spawn returned post-lead scout names before that question. Once the user answers, spawn Message Draft Builder from the same campaign/table basis. If the user chooses filters, also spawn Lead Fit Builder, move to Filter Rules, save rubrics, ask for filter approval, then keep the browser on Filter Leads and show `Filters saved + waiting for message approval` while the message recommendation is reviewed. If filters are skipped, move to Messages/message review.",
344
344
  claude: "After confirm_lead_list copies source rows and the initial campaign-table execution slice exists, ask the filter-choice question immediately. Do not invoke returned post-lead Task/Agent names before that question. Once the user answers, invoke Message Draft Builder from the same campaign/table basis. If the user chooses filters, also invoke Lead Fit Builder, move to Filter Rules, save rubrics, ask for filter approval, then keep the browser on Filter Leads and show `Filters saved + waiting for message approval` while the message recommendation is reviewed. If filters are skipped, move to Messages/message review.",
345
- parentThreadRule: 'Named agents are optional acceleration, but message drafting is not optional. If post-find-leads-message-scout is available, run it as the background Message Draft Builder after the filter-choice answer. If it is absent, do not customer-surface install status; the main thread must execute the same message branch from CampaignOffer state, selected source state, workflowTableId, and initial campaign-table execution slice rows. Local markdown/json files are not normal-path inputs. The filter-choice question is the first post-import user gate; do not load post-lead registries, filter references, or the full generate-messages prompt before it. Message drafting starts after the filter-choice answer, must load get_subskill_prompt({ subskillName: "generate-messages", offset, limit }) until hasMore=false, must read live campaign table state through scoped MCP/product tools, and must reject mismatched selectedLeadListId/workflowTableId/campaign/workspace input. On the filter path, keep the browser on Filter Rules after save_rubrics so the user can approve the saved criteria; only then move to Filter Leads, show `Filters saved + waiting for message approval`, and wait there for message approval. Enrichment, filtering, Generate Message cells, sender setup, sequence attach, and launch wait for template approval on the Use Template path. On the skip path, move to Messages/message review and wait for message approval before enrichment or Settings. Do not render message review from checklist or shortcut instructions; message review requires a messageDraftRecommendation whose basis proves the full generate-messages prompt ran for the current campaign/table execution slice. 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
+ parentThreadRule: 'Named agents are optional acceleration, but message drafting is not optional. If post-find-leads-message-scout is available, run it as the background Message Draft Builder after the filter-choice answer. If it is absent, do not customer-surface install status; the main thread must execute the same message branch from CampaignOffer state, selected source state, workflowTableId, and initial campaign-table execution slice rows. Local markdown/json files are not normal-path inputs. The filter-choice question is the first post-import user gate; do not load post-lead registries, filter references, or the full legacy generate-messages prompt before it. Message drafting starts after the filter-choice answer, must load get_subskill_prompt({ subskillName: "generate-messages-compact" }), must read live campaign table state through scoped MCP/product tools, and must reject mismatched selectedLeadListId/workflowTableId/campaign/workspace input. Load generate-messages-compact/references/examples-critique-revision.md only for critique, revision, or close-call examples. On the filter path, keep the browser on Filter Rules after save_rubrics so the user can approve the saved criteria; only then move to Filter Leads, show `Filters saved + waiting for message approval`, and wait there for message approval. Enrichment, filtering, Generate Message cells, sender setup, sequence attach, and launch wait for template approval on the Use Template path. On the skip path, move to Messages/message review and wait for message approval before enrichment or Settings. Do not render message review from checklist or shortcut instructions; message review requires a messageDraftRecommendation whose basis proves the generate-messages-compact prompt ran for the current campaign/table execution slice. 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.',
346
346
  },
347
347
  };
348
348
  }