@sellable/mcp 0.1.149 → 0.1.151

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 (32) hide show
  1. package/README.md +8 -4
  2. package/agents/post-find-leads-message-scout.md +2 -2
  3. package/agents/source-scout-prospeo-contact.md +12 -5
  4. package/agents/source-scout-sales-nav.md +13 -3
  5. package/dist/tools/leads.d.ts +2 -1
  6. package/dist/tools/leads.js +162 -15
  7. package/dist/tools/prompts.d.ts +1 -0
  8. package/dist/tools/prompts.js +4 -3
  9. package/dist/tools/registry.d.ts +9 -1
  10. package/dist/tools/rows.d.ts +1 -0
  11. package/dist/tools/rows.js +2 -0
  12. package/dist/tools/rubrics.d.ts +55 -23
  13. package/dist/tools/rubrics.js +95 -10
  14. package/package.json +1 -1
  15. package/skills/create-campaign/SKILL.md +57 -29
  16. package/skills/create-campaign/core/providers/apollo.json +4 -2
  17. package/skills/create-campaign/core/providers/prospeo.json +3 -2
  18. package/skills/create-campaign/references/provider-selection-strategy.md +66 -25
  19. package/skills/create-campaign-v2/SKILL.md +67 -37
  20. package/skills/create-campaign-v2/SOUL.md +11 -7
  21. package/skills/create-campaign-v2/core/flow.v2.json +70 -47
  22. package/skills/create-campaign-v2/references/approval-gate-framing.md +7 -8
  23. package/skills/create-campaign-v2/references/sample-validation-loop.md +9 -5
  24. package/skills/create-campaign-v2/references/step-13-import-leads.md +10 -4
  25. package/skills/create-campaign-v2/references/step-15-re-cascade.md +12 -7
  26. package/skills/create-campaign-v2/references/watch-guide-narration.md +16 -13
  27. package/skills/create-campaign-v2-tail/SKILL.md +24 -16
  28. package/skills/providers/apollo.md +8 -1
  29. package/skills/providers/prospeo.md +11 -1
  30. package/skills/providers/sales-nav.md +1 -1
  31. package/skills/research/config.json +9 -0
  32. package/skills/create-campaign-v2/references/message-review-safety-gate.md +0 -162
@@ -33,6 +33,7 @@ type WaitForRubricResultsInput = {
33
33
  tableId?: string;
34
34
  targetCount?: number;
35
35
  minPassedCount?: number;
36
+ minMessagesCount?: number;
36
37
  timeoutMs?: number;
37
38
  intervalMs?: number;
38
39
  includeRows?: boolean;
@@ -114,6 +115,7 @@ export declare const rubricToolDefinitions: ({
114
115
  tableId?: undefined;
115
116
  targetCount?: undefined;
116
117
  minPassedCount?: undefined;
118
+ minMessagesCount?: undefined;
117
119
  timeoutMs?: undefined;
118
120
  intervalMs?: undefined;
119
121
  includeRows?: undefined;
@@ -147,6 +149,7 @@ export declare const rubricToolDefinitions: ({
147
149
  tableId?: undefined;
148
150
  targetCount?: undefined;
149
151
  minPassedCount?: undefined;
152
+ minMessagesCount?: undefined;
150
153
  timeoutMs?: undefined;
151
154
  intervalMs?: undefined;
152
155
  includeRows?: undefined;
@@ -205,6 +208,7 @@ export declare const rubricToolDefinitions: ({
205
208
  tableId?: undefined;
206
209
  targetCount?: undefined;
207
210
  minPassedCount?: undefined;
211
+ minMessagesCount?: undefined;
208
212
  timeoutMs?: undefined;
209
213
  intervalMs?: undefined;
210
214
  includeRows?: undefined;
@@ -263,6 +267,7 @@ export declare const rubricToolDefinitions: ({
263
267
  tableId?: undefined;
264
268
  targetCount?: undefined;
265
269
  minPassedCount?: undefined;
270
+ minMessagesCount?: undefined;
266
271
  timeoutMs?: undefined;
267
272
  intervalMs?: undefined;
268
273
  includeRows?: undefined;
@@ -323,6 +328,7 @@ export declare const rubricToolDefinitions: ({
323
328
  tableId?: undefined;
324
329
  targetCount?: undefined;
325
330
  minPassedCount?: undefined;
331
+ minMessagesCount?: undefined;
326
332
  timeoutMs?: undefined;
327
333
  intervalMs?: undefined;
328
334
  includeRows?: undefined;
@@ -353,6 +359,7 @@ export declare const rubricToolDefinitions: ({
353
359
  tableId?: undefined;
354
360
  targetCount?: undefined;
355
361
  minPassedCount?: undefined;
362
+ minMessagesCount?: undefined;
356
363
  timeoutMs?: undefined;
357
364
  intervalMs?: undefined;
358
365
  includeRows?: undefined;
@@ -383,6 +390,7 @@ export declare const rubricToolDefinitions: ({
383
390
  tableId?: undefined;
384
391
  targetCount?: undefined;
385
392
  minPassedCount?: undefined;
393
+ minMessagesCount?: undefined;
386
394
  timeoutMs?: undefined;
387
395
  intervalMs?: undefined;
388
396
  includeRows?: undefined;
@@ -412,6 +420,10 @@ export declare const rubricToolDefinitions: ({
412
420
  type: string;
413
421
  description: string;
414
422
  };
423
+ minMessagesCount: {
424
+ type: string;
425
+ description: string;
426
+ };
415
427
  timeoutMs: {
416
428
  type: string;
417
429
  description: string;
@@ -508,6 +520,12 @@ export declare function checkRubric(input: CheckRubricInput): Promise<{
508
520
  export declare function waitForRubricResults(input: WaitForRubricResultsInput): Promise<{
509
521
  stats: WorkflowTableStats;
510
522
  rows?: import("./rows.js").LightweightRow[] | undefined;
523
+ messageGeneration?: {
524
+ completed: number;
525
+ passingGeneratedMessages: number;
526
+ minMessagesCount: number;
527
+ floorMet: boolean;
528
+ } | undefined;
511
529
  ready: boolean;
512
530
  attempts: number;
513
531
  elapsedMs: number;
@@ -520,28 +538,14 @@ export declare function waitForRubricResults(input: WaitForRubricResultsInput):
520
538
  targetCount: number;
521
539
  pending?: undefined;
522
540
  };
523
- reason?: undefined;
524
- partialResult?: undefined;
525
- diagnostic?: undefined;
526
- guidance?: undefined;
527
541
  } | {
528
542
  stats: WorkflowTableStats;
529
543
  rows?: import("./rows.js").LightweightRow[] | undefined;
530
- ready: boolean;
531
- partial: boolean;
532
- reason: string;
533
- attempts: number;
534
- elapsedMs: number;
535
- tableId: string;
536
- passRate: {
537
- completed: number;
538
- passed: number;
539
- pending: number;
540
- percent: number;
541
- targetCount: number;
542
- minPassedCount: number;
543
- };
544
544
  partialResult: {
545
+ messagesCount?: number | undefined;
546
+ passingGeneratedMessages?: number | undefined;
547
+ minMessagesCount?: number | undefined;
548
+ messageFloorMet?: boolean | undefined;
545
549
  completed: number;
546
550
  passed: number;
547
551
  pending: number;
@@ -551,23 +555,32 @@ export declare function waitForRubricResults(input: WaitForRubricResultsInput):
551
555
  enoughToDiagnose: boolean;
552
556
  floorMet: boolean;
553
557
  };
554
- diagnostic?: undefined;
555
- guidance?: undefined;
556
- } | {
558
+ messageGeneration?: {
559
+ completed: number;
560
+ passingGeneratedMessages: number;
561
+ minMessagesCount: number;
562
+ floorMet: boolean;
563
+ } | undefined;
557
564
  ready: boolean;
565
+ partial: boolean;
558
566
  reason: string;
559
567
  attempts: number;
560
568
  elapsedMs: number;
561
569
  tableId: string;
562
570
  passRate: {
563
- minPassedCount?: number | undefined;
564
571
  completed: number;
565
572
  passed: number;
566
573
  pending: number;
567
574
  percent: number;
568
575
  targetCount: number;
576
+ minPassedCount: number;
569
577
  };
578
+ } | {
570
579
  partialResult: {
580
+ messagesCount?: number | undefined;
581
+ passingGeneratedMessages?: number | undefined;
582
+ minMessagesCount?: number | undefined;
583
+ messageFloorMet?: boolean | undefined;
571
584
  enoughToDiagnose: boolean;
572
585
  floorMet: boolean;
573
586
  minPassedCount?: number | undefined;
@@ -581,12 +594,31 @@ export declare function waitForRubricResults(input: WaitForRubricResultsInput):
581
594
  totalRows: number;
582
595
  enrichedCount: number | undefined;
583
596
  needsEnrichCount: number | undefined;
584
- messagesCount: number | undefined;
597
+ messagesCount: number;
585
598
  needsApprovalCount: number | undefined;
586
599
  processingCount: number | undefined;
587
600
  failedCount: number | undefined;
588
601
  };
589
602
  guidance: string;
590
603
  stats: WorkflowTableStats | null;
604
+ messageGeneration?: {
605
+ completed: number;
606
+ passingGeneratedMessages: number;
607
+ minMessagesCount: number;
608
+ floorMet: boolean;
609
+ } | undefined;
610
+ ready: boolean;
611
+ reason: string;
612
+ attempts: number;
613
+ elapsedMs: number;
614
+ tableId: string;
615
+ passRate: {
616
+ minPassedCount?: number | undefined;
617
+ completed: number;
618
+ passed: number;
619
+ pending: number;
620
+ percent: number;
621
+ targetCount: number;
622
+ };
591
623
  }>;
592
624
  export {};
@@ -9,6 +9,11 @@ const DEFAULT_INTERVAL_MS = 2000;
9
9
  function sleep(ms) {
10
10
  return new Promise((resolve) => setTimeout(resolve, ms));
11
11
  }
12
+ function countPassingGeneratedMessages(rowSnapshot) {
13
+ if (!rowSnapshot?.rows?.length)
14
+ return 0;
15
+ return rowSnapshot.rows.filter((row) => row.icpPassed === true && Boolean(row.message?.trim())).length;
16
+ }
12
17
  function normalizeRubricItemDefaults(item) {
13
18
  return {
14
19
  ...item,
@@ -393,6 +398,10 @@ export const rubricToolDefinitions = [
393
398
  type: "number",
394
399
  description: "Optional pass floor for bounded create-campaign samples. When this floor is met, the tool returns ready:true with partial:true instead of waiting for every target row to finish.",
395
400
  },
401
+ minMessagesCount: {
402
+ type: "number",
403
+ description: "Optional generated-message floor. When provided with minPassedCount, the tool waits until both floors are met so create-campaign-v2 can review the first passing generated message without waiting for the full batch.",
404
+ },
396
405
  timeoutMs: {
397
406
  type: "number",
398
407
  description: `Max time to wait in ms (default ${DEFAULT_TIMEOUT_MS}).`,
@@ -688,6 +697,7 @@ export async function waitForRubricResults(input) {
688
697
  const intervalMs = Math.max(500, input.intervalMs ?? DEFAULT_INTERVAL_MS);
689
698
  const targetCount = resolveMaxProspects(input.targetCount);
690
699
  const minPassedCount = resolveMinPassedCount(input.minPassedCount);
700
+ const minMessagesCount = resolveMinPassedCount(input.minMessagesCount);
691
701
  const includeRows = input.includeRows !== false;
692
702
  let tableId = input.tableId;
693
703
  if (!tableId && input.campaignOfferId) {
@@ -709,19 +719,34 @@ export async function waitForRubricResults(input) {
709
719
  const totalRows = stats.totalRows ?? 0;
710
720
  const effectiveTarget = totalRows > 0 ? Math.min(targetCount, totalRows) : targetCount;
711
721
  const pending = Math.max(effectiveTarget - completed, 0);
722
+ const messagesCount = stats.messagesCount ?? 0;
712
723
  const failedCount = stats.failedCount ?? 0;
713
724
  const processingCount = stats.processingCount ?? 0;
714
725
  const queuedCount = stats.queuedCount ?? 0;
715
726
  const cancellableCount = stats.cancellableCount ?? 0;
716
727
  const minPassFloorMet = minPassedCount !== null && passed >= minPassedCount;
728
+ const rawMessageFloorMet = minMessagesCount === null || messagesCount >= minMessagesCount;
729
+ let rowSnapshotForMessageCheck = null;
730
+ let passingGeneratedMessagesCount = null;
731
+ if (minMessagesCount !== null && minPassFloorMet && rawMessageFloorMet) {
732
+ rowSnapshotForMessageCheck = await getTableRowsMinimal(tableId, {
733
+ limit: effectiveTarget,
734
+ includeMessages: true,
735
+ });
736
+ passingGeneratedMessagesCount = countPassingGeneratedMessages(rowSnapshotForMessageCheck);
737
+ }
738
+ const messageFloorMet = minMessagesCount === null ||
739
+ (passingGeneratedMessagesCount ?? 0) >= minMessagesCount;
740
+ const earlyFloorMet = minPassFloorMet && messageFloorMet;
717
741
  const unresolvedRowsResolvedAsFailures = pending > 0 && completed + failedCount >= effectiveTarget;
718
742
  const noActiveProcessing = processingCount === 0 && queuedCount === 0 && cancellableCount === 0;
719
- if (completed >= effectiveTarget) {
743
+ if (completed >= effectiveTarget && messageFloorMet) {
720
744
  const percent = completed > 0 ? Math.round((passed / completed) * 100) : 0;
721
745
  const rowSnapshot = includeRows
722
- ? await getTableRowsMinimal(tableId, {
723
- limit: effectiveTarget,
724
- })
746
+ ? (rowSnapshotForMessageCheck ??
747
+ (await getTableRowsMinimal(tableId, {
748
+ limit: effectiveTarget,
749
+ })))
725
750
  : null;
726
751
  return {
727
752
  ready: true,
@@ -735,11 +760,21 @@ export async function waitForRubricResults(input) {
735
760
  targetCount: effectiveTarget,
736
761
  ...(minPassedCount !== null ? { minPassedCount } : {}),
737
762
  },
763
+ ...(minMessagesCount !== null
764
+ ? {
765
+ messageGeneration: {
766
+ completed: messagesCount,
767
+ passingGeneratedMessages: passingGeneratedMessagesCount ?? 0,
768
+ minMessagesCount,
769
+ floorMet: true,
770
+ },
771
+ }
772
+ : {}),
738
773
  ...(rowSnapshot ? { rows: rowSnapshot.rows } : {}),
739
774
  stats,
740
775
  };
741
776
  }
742
- if (minPassFloorMet) {
777
+ if (earlyFloorMet) {
743
778
  const percent = completed > 0 ? Math.round((passed / completed) * 100) : 0;
744
779
  const reason = !noActiveProcessing
745
780
  ? "min_passed_count_met_with_active_processing"
@@ -747,9 +782,10 @@ export async function waitForRubricResults(input) {
747
782
  ? "min_passed_count_met_with_resolved_failures"
748
783
  : "min_passed_count_met_no_active_processing";
749
784
  const rowSnapshot = includeRows
750
- ? await getTableRowsMinimal(tableId, {
751
- limit: effectiveTarget,
752
- })
785
+ ? (rowSnapshotForMessageCheck ??
786
+ (await getTableRowsMinimal(tableId, {
787
+ limit: effectiveTarget,
788
+ })))
753
789
  : null;
754
790
  return {
755
791
  ready: true,
@@ -766,6 +802,16 @@ export async function waitForRubricResults(input) {
766
802
  targetCount: effectiveTarget,
767
803
  minPassedCount,
768
804
  },
805
+ ...(minMessagesCount !== null
806
+ ? {
807
+ messageGeneration: {
808
+ completed: messagesCount,
809
+ passingGeneratedMessages: passingGeneratedMessagesCount ?? 0,
810
+ minMessagesCount,
811
+ floorMet: true,
812
+ },
813
+ }
814
+ : {}),
769
815
  partialResult: {
770
816
  completed,
771
817
  passed,
@@ -775,6 +821,14 @@ export async function waitForRubricResults(input) {
775
821
  minPassedCount,
776
822
  enoughToDiagnose: true,
777
823
  floorMet: true,
824
+ ...(minMessagesCount !== null
825
+ ? {
826
+ messagesCount,
827
+ passingGeneratedMessages: passingGeneratedMessagesCount ?? 0,
828
+ minMessagesCount,
829
+ messageFloorMet: true,
830
+ }
831
+ : {}),
778
832
  },
779
833
  ...(rowSnapshot ? { rows: rowSnapshot.rows } : {}),
780
834
  stats,
@@ -785,9 +839,22 @@ export async function waitForRubricResults(input) {
785
839
  const completed = lastStats?.passRate?.completed ?? 0;
786
840
  const passed = lastStats?.passRate?.passed ?? 0;
787
841
  const percent = completed > 0 ? Math.round((passed / completed) * 100) : 0;
842
+ const messagesCount = lastStats?.messagesCount ?? 0;
788
843
  const totalRows = lastStats?.totalRows ?? 0;
789
844
  const effectiveTarget = totalRows > 0 ? Math.min(targetCount, totalRows) : targetCount;
790
845
  const pending = Math.max(effectiveTarget - completed, 0);
846
+ let timeoutPassingGeneratedMessagesCount = null;
847
+ if (minMessagesCount !== null) {
848
+ try {
849
+ timeoutPassingGeneratedMessagesCount = countPassingGeneratedMessages(await getTableRowsMinimal(tableId, {
850
+ limit: effectiveTarget,
851
+ includeMessages: true,
852
+ }));
853
+ }
854
+ catch {
855
+ timeoutPassingGeneratedMessagesCount = null;
856
+ }
857
+ }
791
858
  return {
792
859
  ready: false,
793
860
  reason: "timeout",
@@ -802,6 +869,16 @@ export async function waitForRubricResults(input) {
802
869
  targetCount: effectiveTarget,
803
870
  ...(minPassedCount !== null ? { minPassedCount } : {}),
804
871
  },
872
+ ...(minMessagesCount !== null
873
+ ? {
874
+ messageGeneration: {
875
+ completed: messagesCount,
876
+ passingGeneratedMessages: timeoutPassingGeneratedMessagesCount ?? 0,
877
+ minMessagesCount,
878
+ floorMet: (timeoutPassingGeneratedMessagesCount ?? 0) >= minMessagesCount,
879
+ },
880
+ }
881
+ : {}),
805
882
  partialResult: {
806
883
  completed,
807
884
  passed,
@@ -811,17 +888,25 @@ export async function waitForRubricResults(input) {
811
888
  ...(minPassedCount !== null ? { minPassedCount } : {}),
812
889
  enoughToDiagnose: completed > 0,
813
890
  floorMet: minPassedCount !== null && passed >= minPassedCount,
891
+ ...(minMessagesCount !== null
892
+ ? {
893
+ messagesCount,
894
+ passingGeneratedMessages: timeoutPassingGeneratedMessagesCount ?? 0,
895
+ minMessagesCount,
896
+ messageFloorMet: (timeoutPassingGeneratedMessagesCount ?? 0) >= minMessagesCount,
897
+ }
898
+ : {}),
814
899
  },
815
900
  diagnostic: {
816
901
  totalRows,
817
902
  enrichedCount: lastStats?.enrichedCount,
818
903
  needsEnrichCount: lastStats?.needsEnrichCount,
819
- messagesCount: lastStats?.messagesCount,
904
+ messagesCount,
820
905
  needsApprovalCount: lastStats?.needsApprovalCount,
821
906
  processingCount: lastStats?.processingCount,
822
907
  failedCount: lastStats?.failedCount,
823
908
  },
824
- guidance: "If this is create-campaign-v2 validate-sample, do not repeat waits indefinitely. Use passRate/stats to diagnose and surface sample_revision_required before Settings when the sample is under the pass floor or messages are incomplete.",
909
+ guidance: "If this is create-campaign-v2 validate-sample, do not repeat waits indefinitely. Use passRate/stats to diagnose and surface sample_revision_required before Settings when the sample is under the pass floor. If minMessagesCount is set, one passing generated message is enough to start review; do not wait for a stronger sample.",
825
910
  stats: lastStats,
826
911
  };
827
912
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.149",
3
+ "version": "0.1.151",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -143,11 +143,34 @@ why. Do not call `search_signals`, `search_sales_nav`, `search_prospeo`,
143
143
  `fetch_post_engagers`, or provider-scoped subagents until the user approves this
144
144
  source plan or explicitly chooses a different source.
145
145
 
146
+ For hiring-led campaigns, do not default to Sales Nav just because the target is
147
+ a role search. Prospeo is the primary lane when the brief asks for companies
148
+ actively hiring specific roles, open-role signals, account/contact coverage, or
149
+ verified contacts at hiring companies because `search_prospeo` supports
150
+ `company_job_posting_hiring_for` and `company_job_posting_quantity`. Signal
151
+ Discovery can be a parallel or fallback lane when relevant hiring conversations
152
+ are likely. Sales Nav is useful for recent LinkedIn activity, role/title
153
+ precision, and referral paths, but it does not provide hiring-by-role filters;
154
+ say that distinction plainly in the source-plan gate.
155
+
146
156
  After scouting, ask for a second approval on the concrete source action. For
147
157
  Signal Discovery, name how many selected posts will be scraped, the target
148
158
  engager/source-candidate volume, and the bounded review-batch size. For Sales
149
- Nav or Prospeo, name the specific approved import lane. Do not call
150
- `import_leads` or `confirm_lead_list` until this second approval is granted.
159
+ Nav or Prospeo, name the specific approved import lane and source lead count.
160
+ Do not call `import_leads` or `confirm_lead_list` until this second approval is
161
+ granted.
162
+
163
+ For Sales Nav and Prospeo, the second gate approves materializing the source
164
+ lead list, not importing only the review batch. Use the first-page/sample review
165
+ to calculate projected good fits: sampled fit rate after conservative cleanup,
166
+ raw pool size, source target, and expected good-fit count. If the projected
167
+ good-fit pool is below the campaign target (normally about 150+ usable
168
+ prospects unless source defaults say otherwise), keep refining/broadening
169
+ filters before asking for import approval. Once it clears target, approve
170
+ `import_leads` with the source-list `targetLeadCount` (up to the raw count or
171
+ provider max). Only after the source list is ready should `confirm_lead_list`
172
+ clone the bounded review batch into the campaign table.
173
+
151
174
  For Signal Discovery, the customer-facing approval card must use the exact
152
175
  action shape "Approve scraping N Signal Discovery posts?" and the chat summary
153
176
  should be a compact `## Source Recommendation` block with:
@@ -158,12 +181,13 @@ should be a compact `## Source Recommendation` block with:
158
181
  - planning floor: continue with Signal Discovery only when sampled/projected
159
182
  fit is at least 10% after cleanup; below that, move to Sales Nav recent
160
183
  activity instead of scraping noisy engagers
161
- - review checkpoint: import the first 25 leads for fit and message review
184
+ - review checkpoint: after the source list exists, clone only the bounded
185
+ review batch into the campaign for fit and message review
162
186
  - a selected-post table with post author/topic, why it fits, and visible
163
187
  engagement
164
188
  - total visible pool and estimated good-fit pool
165
- - first pass: build the source list, then import only the review
166
- batch
189
+ - first pass: build the source list at the approved source-candidate target,
190
+ then clone only the bounded review batch into the campaign
167
191
  - fallback: switch to Sales Nav recent activity if sampled/projected fit falls
168
192
  below 10%, or if the review batch is vendor-heavy, agency-heavy, or off-ICP
169
193
 
@@ -171,7 +195,8 @@ When the user has not supplied a source and multiple source angles are viable,
171
195
  scout those angles as independent branches when the host can actually do it:
172
196
  LinkedIn Engagement / active post engagers (internal `signal-discovery`
173
197
  provider prompt), Sales Nav / title + company filters, and Prospeo Contact /
174
- domains only when relevant. In Codex, explicitly spawn the named custom scouts
198
+ domains, hiring filters, or broad verified-contact expansion when relevant. In
199
+ Codex, explicitly spawn the named custom scouts
175
200
  `source-scout-linkedin-engagement`, `source-scout-sales-nav`, and
176
201
  `source-scout-prospeo-contact` for the credible lanes only when the current host
177
202
  exposes those names; Codex does not infer subagent fan-out from generic source
@@ -193,16 +218,16 @@ that we are pulling sample engagers from these posts to confirm the ICP is
193
218
  actually engaging and the source is viable.
194
219
 
195
220
  After the bounded review batch exists, use the same registry pattern for
196
- post-lead work. The create-campaign-v2 subskill calls
197
- `get_post_find_leads_scout_registry`, then launches the returned
198
- message-generation scout immediately when real subagents are available and the
199
- current session exposes the returned name. If the user chooses filters, launch
200
- the filter-leads scout then and join it with the already-running or completed
201
- message draft before message review. Workflow cell execution still waits for
202
- filter and template approval. AI Generated is an explicit opt-out that cancels
203
- or ignores the background template draft. If the post-lead agents are absent,
204
- the main thread still orchestrates the same branches from the compact context
205
- with MCP tools/assets.
221
+ post-lead work, but do not load that registry or any deep filter/message prompt
222
+ before the filter-choice question. After `confirm_lead_list`, ask add filters
223
+ vs skip filters immediately. Once the user answers, launch the message scout
224
+ from the same campaign/table basis. If the user chooses filters, also launch the
225
+ filter-leads scout, move to Filter Rules, save rubrics, then keep the browser on
226
+ Filter Leads while the message recommendation is reviewed. If the user skips
227
+ filters, move to Messages/message review. Workflow cell execution still waits
228
+ for filter and template approval. AI Generated is an explicit opt-out from the
229
+ template path. If the post-lead agents are absent, the main thread still
230
+ orchestrates the same branches from the compact context with MCP tools/assets.
206
231
 
207
232
  Use rendered Markdown for user review surfaces, not fenced code blocks. Keep
208
233
  lines short, use indexed section labels and bullets, and translate internal
@@ -615,12 +640,12 @@ updates.
615
640
  available. The worker and parent-thread fallback must load the full
616
641
  long-form `generate-messages` prompt with
617
642
  `mcp__sellable__get_subskill_prompt({ subskillName: "generate-messages", offset, limit })`
618
- until `hasMore=false`. The create-campaign message-review safety gate is a
619
- supplemental approval checklist, not a replacement for the long prompt. Use
620
- campaign state, campaign brief content, selected source state, and imported
621
- review-batch rows as the source of truth; do not read stale local markdown
622
- such as `message-validation.md`, inspect the database directly, or synthesize
623
- local validation artifacts from general knowledge.
643
+ until `hasMore=false`. Message review requires Message Draft Builder output:
644
+ do not draft from a checklist, local markdown artifact, or parent-thread
645
+ intuition. Use campaign state, campaign brief content, selected source state, and
646
+ imported review-batch rows as the source of truth; do not read stale local
647
+ markdown such as `message-validation.md`, inspect the database directly, or
648
+ synthesize local validation artifacts from general knowledge.
624
649
  5. Create the campaign shell early with the v1 brief so the user can open the
625
650
  watch link and see useful setup state immediately. Import only the first
626
651
  bounded review batch after the source is attached to the campaign; do not
@@ -631,13 +656,16 @@ updates.
631
656
  call `mcp__sellable__update_campaign({ campaignId, enableICPFilters: true, currentStep: "create-icp-rubric", watchNarration })`
632
657
  so the watched app moves to Filter Rules while rubrics are drafted/saved.
633
658
  After rubrics save, move the watched app to `apply-icp-rubric` / Filter
634
- Leads and say the fit rules are saved; approve the message template next
635
- while the browser stays on Filter Leads. After approval, save the template
636
- to the campaign brief, then queue the bounded review-batch `enrichCellId`
637
- cells to kick off enrichment/filtering. Move to Messages only after at
638
- least one review row passes and Generate Message cells are running or ready.
639
- Product Generate Message cells must not run from the background template
640
- path before that template/token approval.
659
+ Leads and say the fit rules are saved; the background message scout is
660
+ preparing the message recommendation and approval comes next while the
661
+ browser stays on Filter Leads. If filters are skipped, move the watched app to
662
+ Messages/message review and wait for message approval there. After approval,
663
+ save the template to the campaign brief, then queue the bounded review-batch
664
+ `enrichCellId` cells to kick off enrichment/filtering. Move to Messages only
665
+ after at least one review row passes and Generate Message cells are running or
666
+ ready.
667
+ Product Generate Message cells must not run before that template/token
668
+ approval.
641
669
  Do not ask the user to approve the brief before shell creation unless they
642
670
  explicitly requested a no-write draft; the shell itself is the review surface.
643
671
  6. The main thread owns watch navigation. Call
@@ -21,10 +21,12 @@
21
21
  "priority": 3,
22
22
  "useWhen": [
23
23
  "You need technology stack targeting",
24
- "You need bulk domain list targeting",
25
24
  "You need high volume and can accept lower reply rates"
26
25
  ],
27
- "avoidWhen": ["High reply rate and LinkedIn activity are the top priority"],
26
+ "avoidWhen": [
27
+ "The user supplied a company-domain list; use Prospeo domainFilterId first",
28
+ "High reply rate and LinkedIn activity are the top priority"
29
+ ],
28
30
  "reason": "Best for scale and firmographic/technographic filters, but lower LinkedIn activity by default.",
29
31
  "prose": "Since you need to find **{icp}**, I'd recommend **Apollo**.\n\nHere's why:\n- Apollo has technology filters that other sources don't\n- We can find people at companies using {techStack}\n- Heads up: reply rates will be lower since not all leads are active on LinkedIn\n\nShould I search Apollo for {techStack} users?"
30
32
  },
@@ -21,11 +21,12 @@
21
21
  "priority": 2,
22
22
  "useWhen": [
23
23
  "You want Prospeo filters or domain-based search",
24
+ "You need companies hiring for specific roles using job-posting filters",
24
25
  "You need high deliverability from Prospeo"
25
26
  ],
26
27
  "avoidWhen": ["You need LinkedIn activity filters"],
27
- "reason": "Strong for Prospeo-specific filters and domain lists.",
28
- "prose": "Since you're targeting **{icp}**, I'd recommend **Prospeo**.\n\nHere's why:\n- Prospeo has strong domain-based search and deliverability\n- We can find verified contacts at your target companies\n- Good for high-deliverability outreach\n\nShould I search Prospeo for {icp}?"
28
+ "reason": "Strong for hiring-led search, Prospeo-specific filters, verified contacts, and domain lists.",
29
+ "prose": "Since you're targeting **{icp}**, I'd recommend **Prospeo**.\n\nHere's why:\n- Prospeo can filter for companies hiring specific roles with job-posting filters\n- We can pair those company hiring signals with buyer/referrer titles\n- Good for verified-contact coverage when Sales Nav cannot filter by hiring role\n\nShould I search Prospeo for {icp}?"
29
30
  },
30
31
  "askOption": {
31
32
  "label": "Prospeo",