@sellable/mcp 0.1.146 → 0.1.148
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/source-scout-linkedin-engagement.md +7 -3
- package/agents/source-scout-prospeo-contact.md +4 -1
- package/agents/source-scout-sales-nav.md +5 -0
- package/dist/tools/leads.js +12 -3
- package/dist/tools/registry.d.ts +7 -0
- package/dist/tools/rubrics.d.ts +55 -23
- package/dist/tools/rubrics.js +93 -10
- package/package.json +1 -1
- package/skills/create-campaign/SKILL.md +5 -2
- package/skills/create-campaign-v2/SKILL.md +13 -5
- package/skills/create-campaign-v2/SOUL.md +8 -3
- package/skills/create-campaign-v2/core/flow.v2.json +13 -7
- package/skills/create-campaign-v2/references/sample-validation-loop.md +9 -5
- package/skills/create-campaign-v2/references/step-13-import-leads.md +3 -1
- package/skills/create-campaign-v2/references/step-15-re-cascade.md +12 -7
- package/skills/create-campaign-v2/references/watch-guide-narration.md +5 -4
- package/skills/create-campaign-v2-tail/SKILL.md +24 -16
- package/skills/find-leads/SKILL.md +9 -0
- package/skills/providers/prospeo.md +4 -0
- package/skills/providers/sales-nav.md +5 -0
- package/skills/providers/signal-discovery.md +12 -3
|
@@ -57,7 +57,8 @@ currentStep: "signal-discovery" })` before sampling so the watched Signal
|
|
|
57
57
|
leads per 100 engagers before and after a conservative dedupe/cleanup
|
|
58
58
|
factor, required engagers to scrape (`source target / fit rate`), average
|
|
59
59
|
reachable engagers per right-content post, expected usable leads per
|
|
60
|
-
right-content post after dedupe/cleanup,
|
|
60
|
+
right-content post after dedupe/cleanup, posts needed to hit the target, and
|
|
61
|
+
whether sampled/projected fit clears the 10% planning floor.
|
|
61
62
|
8. Select/promote enough right-content posts to plausibly hit the target. If the
|
|
62
63
|
warm Signals pool is useful but too small, return the expected warm range and
|
|
63
64
|
recommend Sales Nav/Prospeo for scale instead of padding with noisy posts.
|
|
@@ -74,8 +75,9 @@ Return a concise structured result with:
|
|
|
74
75
|
engagers, ICP-fit rate as `n/N` plus percentage/range, good-fit prospects per
|
|
75
76
|
100 engagers, required engagers to scrape, average reachable engagers per
|
|
76
77
|
post, expected usable prospects per post after cleanup, posts needed for
|
|
77
|
-
target,
|
|
78
|
-
range, and scale
|
|
78
|
+
target, whether the 10% planning floor clears after cleanup, selected post
|
|
79
|
+
count, review-batch import limit, expected usable lead range, and scale
|
|
80
|
+
fallback
|
|
79
81
|
- `estimated_good_fit_range`
|
|
80
82
|
- `message_context_strength`, directional and source-specific
|
|
81
83
|
- `false_positive_patterns`
|
|
@@ -99,3 +101,5 @@ Evidence standards:
|
|
|
99
101
|
bounded review batch.
|
|
100
102
|
- If `fetch_post_engagers` is unavailable or fails, report that explicitly and mark the estimate lower-confidence.
|
|
101
103
|
- Keep LinkedIn Engagement viable when selected posts can produce roughly 150+ ICP-fit warm prospects before final filtering, even if Sales Nav is more scalable.
|
|
104
|
+
- If sampled/projected fit after cleanup is below 10%, reject the Signals scrape
|
|
105
|
+
path and recommend Sales Nav recent activity as the next source.
|
|
@@ -27,7 +27,7 @@ Process:
|
|
|
27
27
|
1. Read the campaign brief, source intake, kickoff doc, or lane prompt supplied by the parent.
|
|
28
28
|
2. Identify whether this is domain/account targeting or broad persona expansion.
|
|
29
29
|
3. For domain targeting, use or create the standalone `domainFilterId` before searching; never pass raw domains directly into `search_prospeo`.
|
|
30
|
-
4. Run the narrowest useful Prospeo people preview and 1-2 refinements if quality or scale is unclear. Check scale against a default 300+ good-fit target, capped at 2,500 source candidates unless the parent supplies a different target.
|
|
30
|
+
4. Run the narrowest useful Prospeo people preview and 1-2 refinements if quality or scale is unclear. Check scale against a default 300+ good-fit target, capped at 2,500 source candidates unless the parent supplies a different target, and require at least 10% projected good-fit after cleanup.
|
|
31
31
|
5. Call out that Prospeo gives contact/account coverage but usually weaker LinkedIn intent than LinkedIn Engagement or Sales Nav activity slices.
|
|
32
32
|
|
|
33
33
|
Return a concise structured result with:
|
|
@@ -50,4 +50,7 @@ Evidence standards:
|
|
|
50
50
|
|
|
51
51
|
- Never pass raw domains, company website arrays, or company-name arrays into `search_prospeo`.
|
|
52
52
|
- If the user supplied company names rather than domains, report that domain resolution is required before this lane can run safely.
|
|
53
|
+
- Prospeo is the terminal fallback. If projected good-fit after cleanup remains
|
|
54
|
+
below 10% after reasonable refinement, recommend tightening the ICP/source
|
|
55
|
+
direction rather than switching providers again.
|
|
53
56
|
- Treat Prospeo as an account/contact coverage lane, not as proof of fresh LinkedIn intent.
|
|
@@ -35,6 +35,9 @@ Process:
|
|
|
35
35
|
Loosen nonessential filters in order: remove recent-activity first, widen
|
|
36
36
|
adjacent title variants, widen geography/company-size constraints, and only
|
|
37
37
|
keep hard ICP requirements from the brief.
|
|
38
|
+
Also check the 10% planning floor after cleanup. If the best reasonable
|
|
39
|
+
Sales Nav lane remains below 10% projected good-fit, move to Prospeo instead
|
|
40
|
+
of recommending Sales Nav.
|
|
38
41
|
6. Run the baseline plus 1-2 refinements or loosening passes if the first pass
|
|
39
42
|
is noisy or under-scaled. Label the final pool as constrained if it still
|
|
40
43
|
cannot plausibly reach the target after loosening.
|
|
@@ -69,5 +72,7 @@ Evidence standards:
|
|
|
69
72
|
larger than the warm-post path. If Sales Nav is offered for scale, it should
|
|
70
73
|
either project to the target good-fit count or clearly say it is too tight and
|
|
71
74
|
name the next broadening/Prospeo option.
|
|
75
|
+
- If projected good-fit after cleanup is below 10%, do not recommend Sales Nav
|
|
76
|
+
as the winning source; recommend Prospeo as the next provider.
|
|
72
77
|
- Do not hand-wave missing filter IDs.
|
|
73
78
|
- If Sales Nav returns a giant unfiltered pool, discard that result and retry with valid filters before recommending it.
|
package/dist/tools/leads.js
CHANGED
|
@@ -31,6 +31,9 @@ const defaultCampaignSourceDefaults = {
|
|
|
31
31
|
defaultSize: 25,
|
|
32
32
|
minProjectedPass: 5,
|
|
33
33
|
},
|
|
34
|
+
planning: {
|
|
35
|
+
minFitRate: 0.1,
|
|
36
|
+
},
|
|
34
37
|
providers: {
|
|
35
38
|
"signal-discovery": {
|
|
36
39
|
targetGoodFitLeads: 150,
|
|
@@ -280,6 +283,10 @@ function loadCampaignSourceDefaults() {
|
|
|
280
283
|
...defaultCampaignSourceDefaults.reviewBatch,
|
|
281
284
|
...(parsed.reviewBatch ?? {}),
|
|
282
285
|
},
|
|
286
|
+
planning: {
|
|
287
|
+
...defaultCampaignSourceDefaults.planning,
|
|
288
|
+
...(parsed.planning ?? {}),
|
|
289
|
+
},
|
|
283
290
|
providers: {
|
|
284
291
|
...defaultCampaignSourceDefaults.providers,
|
|
285
292
|
...(parsed.providers ?? {}),
|
|
@@ -297,6 +304,7 @@ function getSignalDiscoverySourcePlanDefaults() {
|
|
|
297
304
|
return {
|
|
298
305
|
targetGoodFitLeads: signalDefaults.targetGoodFitLeads,
|
|
299
306
|
defaultFitRate: signalDefaults.defaultFitRate,
|
|
307
|
+
minPlanningFitRate: defaults.planning.minFitRate,
|
|
300
308
|
sourceCandidateTarget,
|
|
301
309
|
reviewBatchSize: defaults.reviewBatch.defaultSize,
|
|
302
310
|
};
|
|
@@ -602,7 +610,7 @@ function formatApproxInteger(value) {
|
|
|
602
610
|
return `~${rounded.toLocaleString("en-US")}`;
|
|
603
611
|
}
|
|
604
612
|
function buildSignalDiscoverySourceRecommendation({ selectedPosts, }) {
|
|
605
|
-
const { targetGoodFitLeads, defaultFitRate, sourceCandidateTarget, reviewBatchSize, } = getSignalDiscoverySourcePlanDefaults();
|
|
613
|
+
const { targetGoodFitLeads, defaultFitRate, minPlanningFitRate, sourceCandidateTarget, reviewBatchSize, } = getSignalDiscoverySourcePlanDefaults();
|
|
606
614
|
const selectedCount = selectedPosts.length;
|
|
607
615
|
const totalEngagement = selectedPosts.reduce((sum, post) => sum + (post.likes ?? 0) + (post.comments ?? 0), 0);
|
|
608
616
|
const tableRows = selectedPosts
|
|
@@ -618,6 +626,7 @@ Use Signal Discovery first.
|
|
|
618
626
|
|
|
619
627
|
**Good-fit target:** ~${targetGoodFitLeads.toLocaleString("en-US")} prospects after cleanup, enrichment, and filters<br>
|
|
620
628
|
**Source-candidate plan:** scrape ~${sourceCandidateTarget.toLocaleString("en-US")} raw engagers using a conservative ${Math.round(defaultFitRate * 100)}% fit-rate assumption<br>
|
|
629
|
+
**Planning floor:** continue with Signal Discovery only when sampled/projected fit is at least ${Math.round(minPlanningFitRate * 100)}% after cleanup; below that, switch to Sales Nav recent activity<br>
|
|
621
630
|
**Review checkpoint:** import the first ${reviewBatchSize.toLocaleString("en-US")} leads into the campaign for fit and message review before scaling
|
|
622
631
|
|
|
623
632
|
### Selected posts
|
|
@@ -637,7 +646,7 @@ This gives enough volume to work toward ~${targetGoodFitLeads.toLocaleString("en
|
|
|
637
646
|
|
|
638
647
|
**First pass:** build the source list, then import only the ${reviewBatchSize.toLocaleString("en-US")}-lead review batch so we can inspect fit and messages before scaling.
|
|
639
648
|
|
|
640
|
-
**Fallback:** if the review batch is too vendor-heavy, agency-heavy, or off-ICP, switch to Sales Nav recent activity.
|
|
649
|
+
**Fallback:** if the sampled/projected fit rate is below ${Math.round(minPlanningFitRate * 100)}%, or if the review batch is too vendor-heavy, agency-heavy, or off-ICP, switch to Sales Nav recent activity.
|
|
641
650
|
|
|
642
651
|
Approval card should say:
|
|
643
652
|
|
|
@@ -1223,7 +1232,7 @@ export const leadToolDefinitions = [
|
|
|
1223
1232
|
},
|
|
1224
1233
|
targetEngagerCount: {
|
|
1225
1234
|
type: "number",
|
|
1226
|
-
description: "Signal Discovery: target number of post engagers/source candidates to scrape. Compute from source target good-fit leads / sampled fit rate; e.g. 150 good fits at 15% fit requires about 1000 engagers. Limits selected posts before starting scrape.",
|
|
1235
|
+
description: "Signal Discovery: target number of post engagers/source candidates to scrape. Compute from source target good-fit leads / sampled fit rate; e.g. 150 good fits at 15% fit requires about 1000 engagers. If the sampled/projected fit rate is below the 10% planning floor after cleanup, switch to the next provider instead of scaling noisy engagers. Limits selected posts before starting scrape.",
|
|
1227
1236
|
},
|
|
1228
1237
|
maxPostsToScrape: {
|
|
1229
1238
|
type: "number",
|
package/dist/tools/registry.d.ts
CHANGED
|
@@ -3702,6 +3702,7 @@ export declare const allTools: ({
|
|
|
3702
3702
|
tableId?: undefined;
|
|
3703
3703
|
targetCount?: undefined;
|
|
3704
3704
|
minPassedCount?: undefined;
|
|
3705
|
+
minMessagesCount?: undefined;
|
|
3705
3706
|
timeoutMs?: undefined;
|
|
3706
3707
|
intervalMs?: undefined;
|
|
3707
3708
|
includeRows?: undefined;
|
|
@@ -3735,6 +3736,7 @@ export declare const allTools: ({
|
|
|
3735
3736
|
tableId?: undefined;
|
|
3736
3737
|
targetCount?: undefined;
|
|
3737
3738
|
minPassedCount?: undefined;
|
|
3739
|
+
minMessagesCount?: undefined;
|
|
3738
3740
|
timeoutMs?: undefined;
|
|
3739
3741
|
intervalMs?: undefined;
|
|
3740
3742
|
includeRows?: undefined;
|
|
@@ -3793,6 +3795,7 @@ export declare const allTools: ({
|
|
|
3793
3795
|
tableId?: undefined;
|
|
3794
3796
|
targetCount?: undefined;
|
|
3795
3797
|
minPassedCount?: undefined;
|
|
3798
|
+
minMessagesCount?: undefined;
|
|
3796
3799
|
timeoutMs?: undefined;
|
|
3797
3800
|
intervalMs?: undefined;
|
|
3798
3801
|
includeRows?: undefined;
|
|
@@ -3851,6 +3854,7 @@ export declare const allTools: ({
|
|
|
3851
3854
|
tableId?: undefined;
|
|
3852
3855
|
targetCount?: undefined;
|
|
3853
3856
|
minPassedCount?: undefined;
|
|
3857
|
+
minMessagesCount?: undefined;
|
|
3854
3858
|
timeoutMs?: undefined;
|
|
3855
3859
|
intervalMs?: undefined;
|
|
3856
3860
|
includeRows?: undefined;
|
|
@@ -3911,6 +3915,7 @@ export declare const allTools: ({
|
|
|
3911
3915
|
tableId?: undefined;
|
|
3912
3916
|
targetCount?: undefined;
|
|
3913
3917
|
minPassedCount?: undefined;
|
|
3918
|
+
minMessagesCount?: undefined;
|
|
3914
3919
|
timeoutMs?: undefined;
|
|
3915
3920
|
intervalMs?: undefined;
|
|
3916
3921
|
includeRows?: undefined;
|
|
@@ -3941,6 +3946,7 @@ export declare const allTools: ({
|
|
|
3941
3946
|
tableId?: undefined;
|
|
3942
3947
|
targetCount?: undefined;
|
|
3943
3948
|
minPassedCount?: undefined;
|
|
3949
|
+
minMessagesCount?: undefined;
|
|
3944
3950
|
timeoutMs?: undefined;
|
|
3945
3951
|
intervalMs?: undefined;
|
|
3946
3952
|
includeRows?: undefined;
|
|
@@ -3971,6 +3977,7 @@ export declare const allTools: ({
|
|
|
3971
3977
|
tableId?: undefined;
|
|
3972
3978
|
targetCount?: undefined;
|
|
3973
3979
|
minPassedCount?: undefined;
|
|
3980
|
+
minMessagesCount?: undefined;
|
|
3974
3981
|
timeoutMs?: undefined;
|
|
3975
3982
|
intervalMs?: undefined;
|
|
3976
3983
|
includeRows?: undefined;
|
package/dist/tools/rubrics.d.ts
CHANGED
|
@@ -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
|
-
|
|
555
|
-
|
|
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
|
|
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 {};
|
package/dist/tools/rubrics.js
CHANGED
|
@@ -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,33 @@ 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
|
+
});
|
|
735
|
+
passingGeneratedMessagesCount = countPassingGeneratedMessages(rowSnapshotForMessageCheck);
|
|
736
|
+
}
|
|
737
|
+
const messageFloorMet = minMessagesCount === null ||
|
|
738
|
+
(passingGeneratedMessagesCount ?? 0) >= minMessagesCount;
|
|
739
|
+
const earlyFloorMet = minPassFloorMet && messageFloorMet;
|
|
717
740
|
const unresolvedRowsResolvedAsFailures = pending > 0 && completed + failedCount >= effectiveTarget;
|
|
718
741
|
const noActiveProcessing = processingCount === 0 && queuedCount === 0 && cancellableCount === 0;
|
|
719
|
-
if (completed >= effectiveTarget) {
|
|
742
|
+
if (completed >= effectiveTarget && messageFloorMet) {
|
|
720
743
|
const percent = completed > 0 ? Math.round((passed / completed) * 100) : 0;
|
|
721
744
|
const rowSnapshot = includeRows
|
|
722
|
-
?
|
|
723
|
-
|
|
724
|
-
|
|
745
|
+
? (rowSnapshotForMessageCheck ??
|
|
746
|
+
(await getTableRowsMinimal(tableId, {
|
|
747
|
+
limit: effectiveTarget,
|
|
748
|
+
})))
|
|
725
749
|
: null;
|
|
726
750
|
return {
|
|
727
751
|
ready: true,
|
|
@@ -735,11 +759,21 @@ export async function waitForRubricResults(input) {
|
|
|
735
759
|
targetCount: effectiveTarget,
|
|
736
760
|
...(minPassedCount !== null ? { minPassedCount } : {}),
|
|
737
761
|
},
|
|
762
|
+
...(minMessagesCount !== null
|
|
763
|
+
? {
|
|
764
|
+
messageGeneration: {
|
|
765
|
+
completed: messagesCount,
|
|
766
|
+
passingGeneratedMessages: passingGeneratedMessagesCount ?? 0,
|
|
767
|
+
minMessagesCount,
|
|
768
|
+
floorMet: true,
|
|
769
|
+
},
|
|
770
|
+
}
|
|
771
|
+
: {}),
|
|
738
772
|
...(rowSnapshot ? { rows: rowSnapshot.rows } : {}),
|
|
739
773
|
stats,
|
|
740
774
|
};
|
|
741
775
|
}
|
|
742
|
-
if (
|
|
776
|
+
if (earlyFloorMet) {
|
|
743
777
|
const percent = completed > 0 ? Math.round((passed / completed) * 100) : 0;
|
|
744
778
|
const reason = !noActiveProcessing
|
|
745
779
|
? "min_passed_count_met_with_active_processing"
|
|
@@ -747,9 +781,10 @@ export async function waitForRubricResults(input) {
|
|
|
747
781
|
? "min_passed_count_met_with_resolved_failures"
|
|
748
782
|
: "min_passed_count_met_no_active_processing";
|
|
749
783
|
const rowSnapshot = includeRows
|
|
750
|
-
?
|
|
751
|
-
|
|
752
|
-
|
|
784
|
+
? (rowSnapshotForMessageCheck ??
|
|
785
|
+
(await getTableRowsMinimal(tableId, {
|
|
786
|
+
limit: effectiveTarget,
|
|
787
|
+
})))
|
|
753
788
|
: null;
|
|
754
789
|
return {
|
|
755
790
|
ready: true,
|
|
@@ -766,6 +801,16 @@ export async function waitForRubricResults(input) {
|
|
|
766
801
|
targetCount: effectiveTarget,
|
|
767
802
|
minPassedCount,
|
|
768
803
|
},
|
|
804
|
+
...(minMessagesCount !== null
|
|
805
|
+
? {
|
|
806
|
+
messageGeneration: {
|
|
807
|
+
completed: messagesCount,
|
|
808
|
+
passingGeneratedMessages: passingGeneratedMessagesCount ?? 0,
|
|
809
|
+
minMessagesCount,
|
|
810
|
+
floorMet: true,
|
|
811
|
+
},
|
|
812
|
+
}
|
|
813
|
+
: {}),
|
|
769
814
|
partialResult: {
|
|
770
815
|
completed,
|
|
771
816
|
passed,
|
|
@@ -775,6 +820,14 @@ export async function waitForRubricResults(input) {
|
|
|
775
820
|
minPassedCount,
|
|
776
821
|
enoughToDiagnose: true,
|
|
777
822
|
floorMet: true,
|
|
823
|
+
...(minMessagesCount !== null
|
|
824
|
+
? {
|
|
825
|
+
messagesCount,
|
|
826
|
+
passingGeneratedMessages: passingGeneratedMessagesCount ?? 0,
|
|
827
|
+
minMessagesCount,
|
|
828
|
+
messageFloorMet: true,
|
|
829
|
+
}
|
|
830
|
+
: {}),
|
|
778
831
|
},
|
|
779
832
|
...(rowSnapshot ? { rows: rowSnapshot.rows } : {}),
|
|
780
833
|
stats,
|
|
@@ -785,9 +838,21 @@ export async function waitForRubricResults(input) {
|
|
|
785
838
|
const completed = lastStats?.passRate?.completed ?? 0;
|
|
786
839
|
const passed = lastStats?.passRate?.passed ?? 0;
|
|
787
840
|
const percent = completed > 0 ? Math.round((passed / completed) * 100) : 0;
|
|
841
|
+
const messagesCount = lastStats?.messagesCount ?? 0;
|
|
788
842
|
const totalRows = lastStats?.totalRows ?? 0;
|
|
789
843
|
const effectiveTarget = totalRows > 0 ? Math.min(targetCount, totalRows) : targetCount;
|
|
790
844
|
const pending = Math.max(effectiveTarget - completed, 0);
|
|
845
|
+
let timeoutPassingGeneratedMessagesCount = null;
|
|
846
|
+
if (minMessagesCount !== null) {
|
|
847
|
+
try {
|
|
848
|
+
timeoutPassingGeneratedMessagesCount = countPassingGeneratedMessages(await getTableRowsMinimal(tableId, {
|
|
849
|
+
limit: effectiveTarget,
|
|
850
|
+
}));
|
|
851
|
+
}
|
|
852
|
+
catch {
|
|
853
|
+
timeoutPassingGeneratedMessagesCount = null;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
791
856
|
return {
|
|
792
857
|
ready: false,
|
|
793
858
|
reason: "timeout",
|
|
@@ -802,6 +867,16 @@ export async function waitForRubricResults(input) {
|
|
|
802
867
|
targetCount: effectiveTarget,
|
|
803
868
|
...(minPassedCount !== null ? { minPassedCount } : {}),
|
|
804
869
|
},
|
|
870
|
+
...(minMessagesCount !== null
|
|
871
|
+
? {
|
|
872
|
+
messageGeneration: {
|
|
873
|
+
completed: messagesCount,
|
|
874
|
+
passingGeneratedMessages: timeoutPassingGeneratedMessagesCount ?? 0,
|
|
875
|
+
minMessagesCount,
|
|
876
|
+
floorMet: (timeoutPassingGeneratedMessagesCount ?? 0) >= minMessagesCount,
|
|
877
|
+
},
|
|
878
|
+
}
|
|
879
|
+
: {}),
|
|
805
880
|
partialResult: {
|
|
806
881
|
completed,
|
|
807
882
|
passed,
|
|
@@ -811,17 +886,25 @@ export async function waitForRubricResults(input) {
|
|
|
811
886
|
...(minPassedCount !== null ? { minPassedCount } : {}),
|
|
812
887
|
enoughToDiagnose: completed > 0,
|
|
813
888
|
floorMet: minPassedCount !== null && passed >= minPassedCount,
|
|
889
|
+
...(minMessagesCount !== null
|
|
890
|
+
? {
|
|
891
|
+
messagesCount,
|
|
892
|
+
passingGeneratedMessages: timeoutPassingGeneratedMessagesCount ?? 0,
|
|
893
|
+
minMessagesCount,
|
|
894
|
+
messageFloorMet: (timeoutPassingGeneratedMessagesCount ?? 0) >= minMessagesCount,
|
|
895
|
+
}
|
|
896
|
+
: {}),
|
|
814
897
|
},
|
|
815
898
|
diagnostic: {
|
|
816
899
|
totalRows,
|
|
817
900
|
enrichedCount: lastStats?.enrichedCount,
|
|
818
901
|
needsEnrichCount: lastStats?.needsEnrichCount,
|
|
819
|
-
messagesCount
|
|
902
|
+
messagesCount,
|
|
820
903
|
needsApprovalCount: lastStats?.needsApprovalCount,
|
|
821
904
|
processingCount: lastStats?.processingCount,
|
|
822
905
|
failedCount: lastStats?.failedCount,
|
|
823
906
|
},
|
|
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
|
|
907
|
+
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
908
|
stats: lastStats,
|
|
826
909
|
};
|
|
827
910
|
}
|
package/package.json
CHANGED
|
@@ -155,14 +155,17 @@ should be a compact `## Source Recommendation` block with:
|
|
|
155
155
|
- good-fit target: about 150 prospects after cleanup, enrichment, and filters
|
|
156
156
|
- source-candidate plan: about 1,000 raw engagers using a conservative 15%
|
|
157
157
|
fit-rate assumption unless sampled data supports a different number
|
|
158
|
+
- planning floor: continue with Signal Discovery only when sampled/projected
|
|
159
|
+
fit is at least 10% after cleanup; below that, move to Sales Nav recent
|
|
160
|
+
activity instead of scraping noisy engagers
|
|
158
161
|
- review checkpoint: import the first 25 leads for fit and message review
|
|
159
162
|
- a selected-post table with post author/topic, why it fits, and visible
|
|
160
163
|
engagement
|
|
161
164
|
- total visible pool and estimated good-fit pool
|
|
162
165
|
- first pass: build the source list, then import only the review
|
|
163
166
|
batch
|
|
164
|
-
- fallback: switch to Sales Nav recent activity if
|
|
165
|
-
vendor-heavy, agency-heavy, or off-ICP
|
|
167
|
+
- fallback: switch to Sales Nav recent activity if sampled/projected fit falls
|
|
168
|
+
below 10%, or if the review batch is vendor-heavy, agency-heavy, or off-ICP
|
|
166
169
|
|
|
167
170
|
When the user has not supplied a source and multiple source angles are viable,
|
|
168
171
|
scout those angles as independent branches when the host can actually do it:
|
|
@@ -49,9 +49,10 @@ for debug output.
|
|
|
49
49
|
12. Sync the approved template into the campaign brief.
|
|
50
50
|
13. After message approval, keep the watched app on Filter Leads while the
|
|
51
51
|
bounded enrichment/filter cascade starts.
|
|
52
|
-
14. Move to Messages only after at least one review row passes
|
|
53
|
-
review
|
|
54
|
-
|
|
52
|
+
14. Move to Messages only after at least one review row passes and one generated
|
|
53
|
+
message is ready for review. Do not wait for a stronger sample once that
|
|
54
|
+
first passing message exists; review it, then hand off to Settings, sender,
|
|
55
|
+
sequence, and explicit launch greenlight.
|
|
55
56
|
|
|
56
57
|
There is no normal approval-packet, commit-gate, atomic-mint, or local
|
|
57
58
|
artifact-validation step. Those belong only to legacy validation/rehearsal
|
|
@@ -149,6 +150,11 @@ sequential by default. Run `source-scout-linkedin-engagement`,
|
|
|
149
150
|
`source-scout-sales-nav`, and `source-scout-prospeo-contact` in parallel only
|
|
150
151
|
when the user explicitly requested source comparison, a prior lane failed, or
|
|
151
152
|
the first viable lane is borderline and a cheap fallback check is needed.
|
|
153
|
+
Use a 10% planning floor after conservative cleanup. If Signal Discovery falls
|
|
154
|
+
below that floor, move to Sales Nav. If Sales Nav falls below that floor after
|
|
155
|
+
reasonable refinement, move to Prospeo. Prospeo is the terminal fallback; if it
|
|
156
|
+
also falls below 10%, tighten the ICP/source direction instead of inventing
|
|
157
|
+
another provider.
|
|
152
158
|
|
|
153
159
|
After scouting, show a second approval gate for the concrete source action. For
|
|
154
160
|
Signal Discovery, this gate must say how many selected posts will be scraped,
|
|
@@ -175,6 +181,7 @@ Use Signal Discovery first.
|
|
|
175
181
|
|
|
176
182
|
**Good-fit target:** ~150 prospects after cleanup, enrichment, and filters<br>
|
|
177
183
|
**Source-candidate plan:** scrape ~1,000 raw engagers using a conservative 15% fit-rate assumption<br>
|
|
184
|
+
**Planning floor:** continue only when sampled/projected fit is at least 10% after cleanup; below that, switch to Sales Nav recent activity<br>
|
|
178
185
|
**Review checkpoint:** import the first 25 leads into the campaign for fit and message review before scaling<br>
|
|
179
186
|
|
|
180
187
|
### Selected posts
|
|
@@ -199,8 +206,9 @@ AI-native sales workflows.
|
|
|
199
206
|
**First pass:** build the source list, then import only the 25-lead review
|
|
200
207
|
batch so we can inspect fit and messages before scaling.
|
|
201
208
|
|
|
202
|
-
**Fallback:** if
|
|
203
|
-
off-ICP, switch to Sales Nav recent
|
|
209
|
+
**Fallback:** if sampled/projected fit falls below 10%, or if the review batch
|
|
210
|
+
is too vendor-heavy, agency-heavy, or off-ICP, switch to Sales Nav recent
|
|
211
|
+
activity.
|
|
204
212
|
```
|
|
205
213
|
|
|
206
214
|
A source recommendation must show concrete evidence: source lane, filters or
|
|
@@ -239,9 +239,9 @@ for source approval before import.
|
|
|
239
239
|
For Signal Discovery, the second gate is not "approve source" in the abstract.
|
|
240
240
|
It is a concrete scrape approval: show a compact `## Source Recommendation`
|
|
241
241
|
with the ~150 good-fit prospect goal, 15% raw-engager assumption, selected-post
|
|
242
|
-
table, total visible pool, estimated good-fit pool,
|
|
243
|
-
batch, and fallback. The approval question should be "Approve
|
|
244
|
-
Discovery posts?"
|
|
242
|
+
table, total visible pool, estimated good-fit pool, 10% planning floor,
|
|
243
|
+
first-pass review batch, and fallback. The approval question should be "Approve
|
|
244
|
+
scraping N Signal Discovery posts?"
|
|
245
245
|
|
|
246
246
|
That scrape approval is single-use. Once the user approves it, do not replay
|
|
247
247
|
the source card or ask for the same approval again. Acknowledge the approval in
|
|
@@ -257,6 +257,11 @@ borderline and needs a second source to avoid a bad recommendation. The fallback
|
|
|
257
257
|
order must stay plain: use Sales Nav to find ICP people who are actively posting
|
|
258
258
|
on LinkedIn; if that is not enough, search Sales Nav by titles; if that is still
|
|
259
259
|
not enough, use Prospeo for a broader account/contact path.
|
|
260
|
+
Use a 10% planning floor after conservative cleanup. If Signal Discovery falls
|
|
261
|
+
below that floor, move to Sales Nav. If Sales Nav falls below that floor after
|
|
262
|
+
reasonable refinement, move to Prospeo. Prospeo is the terminal fallback; if it
|
|
263
|
+
also falls below 10%, tighten the ICP/source direction instead of inventing
|
|
264
|
+
another provider.
|
|
260
265
|
|
|
261
266
|
Source-angle comparison should be real, not implied. Call
|
|
262
267
|
`get_source_scout_registry` before dispatching source scouts and use the
|
|
@@ -1124,8 +1124,14 @@
|
|
|
1124
1124
|
"cellSource": "pending_generate_message_cells_for_passing_rows using approved_campaign_brief_template"
|
|
1125
1125
|
},
|
|
1126
1126
|
{
|
|
1127
|
-
"tool": "
|
|
1128
|
-
"purpose": "
|
|
1127
|
+
"tool": "wait_for_rubric_results",
|
|
1128
|
+
"purpose": "wait_for_first_passing_generated_message",
|
|
1129
|
+
"requiredValues": {
|
|
1130
|
+
"includeRows": false,
|
|
1131
|
+
"minPassedCount": 1,
|
|
1132
|
+
"minMessagesCount": 1
|
|
1133
|
+
},
|
|
1134
|
+
"readVia": "stats_only_tool_result"
|
|
1129
1135
|
},
|
|
1130
1136
|
{
|
|
1131
1137
|
"action": "observe_generate_message_results",
|
|
@@ -1169,25 +1175,24 @@
|
|
|
1169
1175
|
"currentStep": "auto-execute-messaging",
|
|
1170
1176
|
"watchNarration.stage": "review-ready"
|
|
1171
1177
|
},
|
|
1172
|
-
"watchNarrationRule": "Say
|
|
1178
|
+
"watchNarrationRule": "Say the first passing generated message is ready in Messages; next is review before Settings."
|
|
1173
1179
|
},
|
|
1174
1180
|
{
|
|
1175
1181
|
"action": "ask_generated_message_review_choice",
|
|
1176
1182
|
"uses": "request_user_input",
|
|
1177
1183
|
"choices": [
|
|
1178
|
-
"Approve generated
|
|
1184
|
+
"Approve generated message and continue to Settings",
|
|
1179
1185
|
"Revise filters",
|
|
1180
1186
|
"Revise message template",
|
|
1181
1187
|
"Pause here"
|
|
1182
|
-
]
|
|
1183
|
-
"rule": "Do not advance to Settings until the generated review-batch messages have been reviewed and approved."
|
|
1188
|
+
]
|
|
1184
1189
|
}
|
|
1185
1190
|
],
|
|
1186
1191
|
"allowedTools": [
|
|
1187
1192
|
"get_subskill_asset",
|
|
1188
1193
|
"get_rows_minimal",
|
|
1189
1194
|
"queue_cells",
|
|
1190
|
-
"
|
|
1195
|
+
"wait_for_rubric_results",
|
|
1191
1196
|
"update_campaign",
|
|
1192
1197
|
"AskUserQuestion",
|
|
1193
1198
|
"request_user_input"
|
|
@@ -1201,6 +1206,7 @@
|
|
|
1201
1206
|
"hardRules": [
|
|
1202
1207
|
"critique_failure_never_escalates",
|
|
1203
1208
|
"critique_sample_size_bounded_by_config",
|
|
1209
|
+
"first_passing_generated_message_unblocks_review",
|
|
1204
1210
|
"critics_fixed_at_targeting_copy_voice",
|
|
1205
1211
|
"synthesis_enforces_phase_84_token_contract",
|
|
1206
1212
|
"opus_reserved_for_highest_value_subset",
|
|
@@ -60,9 +60,10 @@ auto-revise leads.
|
|
|
60
60
|
explicit batch count anyway so future larger expansion batches do not
|
|
61
61
|
accidentally stop early
|
|
62
62
|
(see §Known Tool Behaviors #3)
|
|
63
|
-
- minPassedCount=1 means one passing filtered row unblocks Step 15
|
|
64
|
-
|
|
65
|
-
before
|
|
63
|
+
- minPassedCount=1 means one passing filtered row unblocks Step 15 Generate
|
|
64
|
+
Message observation. Step 15 then waits only for one generated message
|
|
65
|
+
(`minMessagesCount=1`) before review. Do not wait for all sample rows to
|
|
66
|
+
finish before messages start.
|
|
66
67
|
|
|
67
68
|
7. call `wait_for_rubric_results` with `includeRows=false`; extract ONLY:
|
|
68
69
|
- ready: boolean
|
|
@@ -101,7 +102,9 @@ auto-revise leads.
|
|
|
101
102
|
10. branch:
|
|
102
103
|
if passInSample >= 1:
|
|
103
104
|
proceed to Step 15 (auto-execute-messaging) with currently passing rows
|
|
104
|
-
so Generate Message can start without waiting for the full sample
|
|
105
|
+
so Generate Message can start without waiting for the full sample. Once
|
|
106
|
+
one passing generated message is ready, stop for user review instead of
|
|
107
|
+
waiting for a stronger sample.
|
|
105
108
|
else:
|
|
106
109
|
diagnose (see Brief-vs-List Diagnosis below)
|
|
107
110
|
revisionRound += 1
|
|
@@ -179,7 +182,8 @@ processing makes the experience feel frozen.
|
|
|
179
182
|
|
|
180
183
|
Workaround: treat timeout stats as a partial sample. If at least one row has
|
|
181
184
|
passed, move to Step 15 and observe or queue Generate Message for the passing
|
|
182
|
-
rows
|
|
185
|
+
rows; Step 15 stops when one generated message is ready. If zero rows have
|
|
186
|
+
passed and no active processing is visible, stop at
|
|
183
187
|
`Status: sample-needs-revision` before Settings. Show the completed / passed /
|
|
184
188
|
pending counts and ask whether to revise source, revise filter/rubric, or wait
|
|
185
189
|
once only if active processing is still visible.
|
|
@@ -125,7 +125,9 @@ fallback fit rate is 15%, so the default source-candidate plan is about 1,000
|
|
|
125
125
|
raw engagers. If the sampled fit rate is stronger or weaker, use the sampled
|
|
126
126
|
rate with a conservative cleanup factor, cap the source candidates at the
|
|
127
127
|
approved provider limit, and select only enough posts to reach that engager
|
|
128
|
-
count. The
|
|
128
|
+
count. The planning floor is 10% projected fit after cleanup; if Signal
|
|
129
|
+
Discovery falls below that floor, do not import the source list. Route back to
|
|
130
|
+
find-leads and move to Sales Nav recent activity instead. The subsequent
|
|
129
131
|
`confirm_lead_list` call still uses `targetLeadCount: <importLimit>` so only the
|
|
130
132
|
bounded review batch enters the campaign table.
|
|
131
133
|
|
|
@@ -4,15 +4,16 @@ This reference governs Step 15 (`auto-execute-messaging`) re-cascade
|
|
|
4
4
|
behavior when Step 14's validate-sample loop graduates additional rows
|
|
5
5
|
from pending → passed after the first Generate Message cells have already run.
|
|
6
6
|
|
|
7
|
-
Load whenever Step 15 is about to transition
|
|
8
|
-
`awaiting-user-greenlight`, and on every resume into Step 15.
|
|
7
|
+
Load whenever Step 15 is about to ask for generated-message review or transition
|
|
8
|
+
to `awaiting-user-greenlight`, and on every resume into Step 15.
|
|
9
9
|
|
|
10
10
|
## Principle
|
|
11
11
|
|
|
12
|
-
Generate Message cells are cascade-scoped. If Step 14's rubric flips rows
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
Generate Message cells are cascade-scoped. If Step 14's rubric flips rows from
|
|
13
|
+
pending → passed AFTER Step 15 first observes messages for the review batch, the
|
|
14
|
+
new rows can sit pending with no message. That must not block the first review
|
|
15
|
+
handoff. Step 15 opens review as soon as one passing generated message exists;
|
|
16
|
+
late-passed rows can be re-cascaded after explicit user continuation or resume.
|
|
16
17
|
|
|
17
18
|
Observed on manual Phase-85 signal-discovery run (2026-04-20): 15 new
|
|
18
19
|
rows graduated pending → passed mid-tail while `messagesCount` stayed
|
|
@@ -22,13 +23,17 @@ flat at 257. The new rows never got messages.
|
|
|
22
23
|
|
|
23
24
|
Re-cascade runs whenever ALL of the following are true:
|
|
24
25
|
|
|
25
|
-
1.
|
|
26
|
+
1. The user has already approved the first generated message or explicitly asked
|
|
27
|
+
to continue processing more review-batch rows.
|
|
26
28
|
2. A subsequent check of rubric state shows rows that were pending at
|
|
27
29
|
first message-generation pass are now passed.
|
|
28
30
|
3. Those newly-passed rows do NOT yet have generated messages
|
|
29
31
|
(`messagesCount` flat relative to pre-cascade).
|
|
30
32
|
4. Step 15 has NOT yet transitioned to `awaiting-user-greenlight`.
|
|
31
33
|
|
|
34
|
+
Before that first generated-message approval, do not run this loop just to wait
|
|
35
|
+
for a bigger sample. One passing generated message is enough for review.
|
|
36
|
+
|
|
32
37
|
If condition 4 already fired (i.e. we're in Step 16), the re-cascade
|
|
33
38
|
runs on resume into Step 15 if validate-sample was re-entered and new
|
|
34
39
|
rows graduated.
|
|
@@ -252,16 +252,17 @@ Template approved, bounded filter test running:
|
|
|
252
252
|
"stage": "fit-message",
|
|
253
253
|
"headline": "Template saved",
|
|
254
254
|
"visibleState": "The browser stays on Filter Leads while the bounded enrichment and filter test runs.",
|
|
255
|
-
"agentIntent": "Codex saved the approved message template, queued the review-batch Enrich Prospect cells, and is waiting for
|
|
256
|
-
"nextAction": "
|
|
255
|
+
"agentIntent": "Codex saved the approved message template, queued the review-batch Enrich Prospect cells, and is waiting for one passing row with one generated message before moving to review.",
|
|
256
|
+
"nextAction": "Review the first passing generated message"
|
|
257
257
|
}
|
|
258
258
|
```
|
|
259
259
|
|
|
260
260
|
Do not move to Messages immediately after `approve-message`. The visible route
|
|
261
261
|
is already Filter Leads after `save_rubrics`; approving the message only unlocks
|
|
262
262
|
the bounded cascade from that screen. Move to Messages only once at least one
|
|
263
|
-
review-batch row passes and
|
|
264
|
-
|
|
263
|
+
review-batch row passes and one generated message is ready for review. Do not
|
|
264
|
+
wait for the rest of the review batch or a stronger sample before asking the
|
|
265
|
+
user to approve the generated message.
|
|
265
266
|
|
|
266
267
|
Messages waiting for template:
|
|
267
268
|
|
|
@@ -58,11 +58,12 @@ Step 14 — kick bounded cascade + observe sample
|
|
|
58
58
|
passing filtered row exists.)
|
|
59
59
|
|
|
60
60
|
Step 15 — observe messaging
|
|
61
|
-
|
|
61
|
+
wait_for_rubric_results({ minPassedCount: 1, minMessagesCount: 1, includeRows: false })
|
|
62
|
+
get_rows_minimal # confirm the first passing generated message exists
|
|
62
63
|
(rare) queue_cells on any pending Generate Message cells
|
|
63
64
|
token-contract spot check via get_rows
|
|
64
65
|
update_campaign(currentStep=auto-execute-messaging) with review-ready narration
|
|
65
|
-
ask the user to approve generated
|
|
66
|
+
ask the user to approve the generated message before Settings
|
|
66
67
|
only after approval: update_campaign(currentStep=awaiting-user-greenlight)
|
|
67
68
|
(generate_messages is NOT an MCP tool; messages come from the cascade)
|
|
68
69
|
|
|
@@ -291,8 +292,10 @@ Do not route to a visible `validate-sample` step. Full decision tree lives in
|
|
|
291
292
|
review batch only. After `save_rubrics` and the approved message template are
|
|
292
293
|
persisted, Step 14 queues the review-batch Enrich Prospect cells, waits until
|
|
293
294
|
filter results start landing, then moves to message observation as soon as one
|
|
294
|
-
row passes.
|
|
295
|
-
|
|
295
|
+
row passes. Step 15 opens review as soon as one passing generated message
|
|
296
|
+
exists. Do not wait for a larger or stronger sample once that first passing
|
|
297
|
+
message is ready. It does NOT call `check_rubric`,
|
|
298
|
+
`bulk_enrich_with_prospeo`, or any other direct enrichment/scoring tool.
|
|
296
299
|
|
|
297
300
|
Shape:
|
|
298
301
|
|
|
@@ -307,6 +310,7 @@ projectedPass = round(passInSample / sampleSize * importLimit)
|
|
|
307
310
|
if wait_for_rubric_results.ready === true and passRate.passed >= 1:
|
|
308
311
|
advance to Step 15 to observe or queue Generate Message for currently passing rows
|
|
309
312
|
do not wait for every sample row to finish before message generation starts
|
|
313
|
+
stop for review as soon as one passing generated message is ready
|
|
310
314
|
else if wait_for_rubric_results.ready === false and reason === "timeout":
|
|
311
315
|
use the partial passRate/stats as the sample diagnostic
|
|
312
316
|
if passRate.passed >= 1:
|
|
@@ -382,17 +386,20 @@ Template`. If it does not, fail before the cascade runs. Do not repair
|
|
|
382
386
|
already (cascade auto-fired it). If it is still `pending`, queue
|
|
383
387
|
it explicitly: `queue_cells({ tableId, cellIds:
|
|
384
388
|
<generateMessageCellIds> })`.
|
|
385
|
-
3. `
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
+
3. `wait_for_rubric_results({ tableId, targetCount: reviewBatchSize,
|
|
390
|
+
minPassedCount: 1, minMessagesCount: 1, includeRows: false })` until the
|
|
391
|
+
first passing generated message is ready. Do not wait for every passing row's
|
|
392
|
+
Generate Message cell, and do not add "one more wait" for a stronger sample.
|
|
393
|
+
Remaining review-batch rows can continue processing in the background.
|
|
394
|
+
4. Read the first ready generated message back via `get_rows` (full) and
|
|
395
|
+
sanity-check that sample against the Phase 84 token contract: no unresolved
|
|
389
396
|
`{{tokens}}`, no invented proof, one sentence per line, etc.
|
|
390
397
|
5. If the sample fails the token contract, diagnose brief-vs-list
|
|
391
398
|
(same revision loop as Step 14) and escalate if over
|
|
392
399
|
`maxRevisionRounds`.
|
|
393
|
-
6. On success, keep `currentStep: "auto-execute-messaging"` and ask the user
|
|
394
|
-
|
|
395
|
-
the campaign to Settings / `awaiting-user-greenlight`.
|
|
400
|
+
6. On success, keep `currentStep: "auto-execute-messaging"` and ask the user to
|
|
401
|
+
approve the generated message before continuing to Settings. Only that
|
|
402
|
+
approval may move the campaign to Settings / `awaiting-user-greenlight`.
|
|
396
403
|
|
|
397
404
|
**Do NOT hand-write message bodies via `update_cell`.** `update_cell`
|
|
398
405
|
is reachable for legitimate operator overrides AFTER the tail hands
|
|
@@ -432,15 +439,16 @@ strings, which is why Step 16 requires Step 15 to be complete.
|
|
|
432
439
|
4. Check `messaging.tokenContract`. When `strict`, reject any sample
|
|
433
440
|
message that contains unresolved or unsupported tokens (including
|
|
434
441
|
any critique rewrite that tried to introduce one).
|
|
435
|
-
5. If the
|
|
436
|
-
stop at the review-batch handoff. Do NOT
|
|
437
|
-
|
|
442
|
+
5. If the first passing generated message passes the token contract (and
|
|
443
|
+
critique when enabled), stop at the review-batch handoff. Do NOT wait for the
|
|
444
|
+
remaining review-batch rows and do NOT scale to the full source list before
|
|
445
|
+
explicit user expansion approval.
|
|
438
446
|
6. If the sample fails the token contract or critique, diagnose +
|
|
439
447
|
loop the same way Step 14 does (brief-vs-list), subject to the same
|
|
440
448
|
`maxRevisionRounds` cap.
|
|
441
449
|
7. On success, keep `currentStep: "auto-execute-messaging"`, show that the
|
|
442
|
-
|
|
443
|
-
Settings. Only after the user approves
|
|
450
|
+
first passing generated message is ready in Messages, and ask for approval
|
|
451
|
+
before Settings. Only after the user approves the generated message should
|
|
444
452
|
`update_campaign({ campaignId, currentStep: "awaiting-user-greenlight" })`
|
|
445
453
|
run.
|
|
446
454
|
|
|
@@ -321,6 +321,9 @@ Use first when the value is in conversation opportunity, competitor engagers, co
|
|
|
321
321
|
- Sample a representative first page only, scoring the first 25-40 engagers across the chosen posts against a rough yes/no headline rubric or `headlineICPCriteria`.
|
|
322
322
|
- Use headline and display-name cues only for the spot-check sample; do not enrich people during this phase.
|
|
323
323
|
- Base `estimatedReachableLeads` on the sampled engager pass rate, not only on a guessed discount from post themes.
|
|
324
|
+
- Use a 10% planning floor after conservative cleanup. If the sampled/projected
|
|
325
|
+
fit rate is below 10%, do not scale Signals; move to Sales Nav recent activity
|
|
326
|
+
instead.
|
|
324
327
|
- A Signals lane with ~150+ estimated ICP-fit reachable engagers from selected
|
|
325
328
|
posts is viable for a focused warm campaign, even if Sales Nav is more
|
|
326
329
|
scalable. Do not discard that lane solely for being smaller. If post-level
|
|
@@ -384,6 +387,9 @@ Use first when LinkedIn activity plus tighter role / company filters matter.
|
|
|
384
387
|
mark Sales Nav as a provider/tool issue and do not include it as a winning
|
|
385
388
|
source.
|
|
386
389
|
- If quality is good but scale is too small, widen the lane by adding/removing roles, expanding industries, widening headcount bands, or relaxing activity constraints.
|
|
390
|
+
- Use the same 10% planning floor after cleanup. If the final Sales Nav lane is
|
|
391
|
+
projected below 10% good-fit after reasonable refinement, move to Prospeo
|
|
392
|
+
rather than importing a noisy Sales Nav list.
|
|
387
393
|
- Use as many smart refinement steps as needed within the remaining probe budget instead of returning the first under-scaled recipe.
|
|
388
394
|
- If the lane is meant to support scalable outbound, do not stop at a merely borderline workable result when obvious expansion steps remain.
|
|
389
395
|
- If you can name a specific next Sales Nav refinement that is still allowed by the probe budget, execute it before returning instead of listing it only as a recommendation.
|
|
@@ -404,6 +410,9 @@ Use first for broad persona expansion, ABM/domain targeting, hiring-led targetin
|
|
|
404
410
|
- If you widen with seniority labels such as `Head` or `Director`, keep a matching department/function constraint and inspect the sample for off-function leakage such as `Head of Social Media`, `Head of IT`, or `Director of Finance`.
|
|
405
411
|
- If quality is poor, tighten titles, company shape, seniority, geography, or hiring filters.
|
|
406
412
|
- If quality is good but scale is too small, widen titles, industries, company-size bands, or account scope.
|
|
413
|
+
- Prospeo is the terminal fallback for this chain. If projected fit is still
|
|
414
|
+
below the 10% planning floor after reasonable Prospeo refinement, stop and ask
|
|
415
|
+
for a tighter ICP/source direction instead of inventing another provider.
|
|
407
416
|
- When ABM/domain targeting exists, prefer refining around the account set before broadening away from it.
|
|
408
417
|
- Use as many smart refinement steps as needed within the remaining probe budget instead of returning the first under-scaled recipe.
|
|
409
418
|
- If the lane is meant to support scalable outbound, do not stop at a merely borderline workable result when obvious expansion steps remain.
|
|
@@ -63,6 +63,10 @@ search_prospeo({
|
|
|
63
63
|
- Use `import_leads` with `provider: \"prospeo\"` and the `searchId` to create a lead list and start import.
|
|
64
64
|
- **IMPORTANT:** If `import_leads` returns `needsModeSelection: true`, use `AskUserQuestion` to ask "add to existing leads or replace?" Do NOT assume.
|
|
65
65
|
- Default source target is 300+ good-fit leads, capped at 2,500 source candidates for now.
|
|
66
|
+
- Apply the campaign source planning floor: the sampled/projected good-fit rate
|
|
67
|
+
after cleanup should be at least 10%. Prospeo is the terminal fallback; if the
|
|
68
|
+
best reasonable Prospeo lane is still below 10%, tighten the ICP/source
|
|
69
|
+
direction instead of switching to another provider.
|
|
66
70
|
- After the list finishes and the user confirms it looks good, call `confirm_lead_list` with the `jobId` and the review-batch `targetLeadCount` from the active campaign defaults to import only the review batch into the campaign table.
|
|
67
71
|
- `import_leads` owns the watched move to `confirm-lead-list` after a lead list/job exists.
|
|
68
72
|
- `confirm_lead_list` owns the watched move to `filter-choice` after the bounded review batch exists.
|
|
@@ -96,6 +96,11 @@ Example search name: "Active CXOs at 201-500 companies"
|
|
|
96
96
|
- < 1,000 = remove a filter or widen criteria
|
|
97
97
|
- > 10,000 = add more filters
|
|
98
98
|
|
|
99
|
+
Also apply the campaign source planning floor: the sampled/projected good-fit
|
|
100
|
+
rate after cleanup should be at least 10%. If the best reasonable Sales Nav
|
|
101
|
+
lane remains below 10%, do not present it as the winning source; move to
|
|
102
|
+
Prospeo for broader account/contact coverage.
|
|
103
|
+
|
|
99
104
|
4. FINAL SEARCH + CONFIRM - Execute winning strategy, ask before saving
|
|
100
105
|
|
|
101
106
|
- Run the final search with the best filter combination
|
|
@@ -24,7 +24,7 @@ When the user asks to find posts or start searching, **IMMEDIATELY begin Round 1
|
|
|
24
24
|
|
|
25
25
|
<lead_target>
|
|
26
26
|
|
|
27
|
-
- **Default: provider max import count** (use this unless user specifies otherwise; see `lead-import-limits.json`, currently
|
|
27
|
+
- **Default: provider max import count** (use this unless user specifies otherwise; see `lead-import-limits.json`, currently 2,500)
|
|
28
28
|
- **Maximum: provider max import count**
|
|
29
29
|
- Quality > Quantity: a few hundred active users > 1000 potentially inactive profiles
|
|
30
30
|
- Focus on ACTIVE platform users who will actually see and respond to outreach
|
|
@@ -104,6 +104,9 @@ You must estimate:
|
|
|
104
104
|
- `ceil(targetGoodFitLeads / sampledFitRate)`; for example 15 good-fit
|
|
105
105
|
prospects per 100 engagers means a 150-good-fit source target needs about
|
|
106
106
|
1,000 engagers scraped
|
|
107
|
+
- `planningFloor`
|
|
108
|
+
- minimum acceptable sampled/projected fit rate after conservative cleanup;
|
|
109
|
+
default 10%. Below this, do not scale Signal Discovery.
|
|
107
110
|
- `projectedRange`
|
|
108
111
|
- the conservative range implied by the observed sample
|
|
109
112
|
|
|
@@ -130,7 +133,10 @@ Use conservative logic:
|
|
|
130
133
|
- preferred: "`sampledCount`: 40, `passCount`: 9, `passRate`: ~22%, `goodFitPer100Engagers`: ~22 before cleanup / ~13-16 after cleanup, `requiredEngagersToScrape`: ~940-1,150 for a 150-good-fit target after cleanup, `avgReachableEngagersPerPost`: 240, `goodFitProspectsPerPost`: ~31-38, `postsNeededForTarget`: ~4-5, `recentStrongPostCount`: 15-25, `freshEnoughPostCount`: 8-12, `projectedRange`: 150-190 from selected posts"
|
|
131
134
|
- fallback: "I could not fetch engagers, so this is inferred from post and author quality only"
|
|
132
135
|
5. If the evidence is too weak, say so and return a low-confidence estimate instead of pretending precision.
|
|
133
|
-
6.
|
|
136
|
+
6. If sampled/projected fit after cleanup is below the 10% planning floor,
|
|
137
|
+
reject the Signal Discovery scrape path and recommend Sales Nav recent
|
|
138
|
+
activity as the next source.
|
|
139
|
+
7. Do not manually enumerate dozens of engagers in the final answer. Summarize the sample by `sampledCount`, `passCount`, `passRate`, `projectedRange`, and 3-6 representative examples.
|
|
134
140
|
|
|
135
141
|
</yield_estimation>
|
|
136
142
|
|
|
@@ -389,12 +395,15 @@ recommendation, estimate source capacity from real sample math:
|
|
|
389
395
|
- average reachable engagers per right-content post
|
|
390
396
|
- expected good-fit leads per selected post after dedupe/cleanup
|
|
391
397
|
- posts needed to reach the target
|
|
398
|
+
- whether sampled/projected fit clears the 10% planning floor
|
|
392
399
|
|
|
393
400
|
Then select the smallest right-content post set that plausibly hits the source
|
|
394
401
|
target. Do not scrape every promoted sample post by default; promoted sampling
|
|
395
402
|
state and final scrape plan are separate. If the math says the warm post lane
|
|
396
403
|
only supports a smaller first batch, say that and name the Sales Nav or Prospeo
|
|
397
|
-
scale fallback rather than padding the selection with noisy posts.
|
|
404
|
+
scale fallback rather than padding the selection with noisy posts. If the
|
|
405
|
+
sampled/projected fit rate is below 10% after cleanup, do not call
|
|
406
|
+
`import_leads`; move to Sales Nav recent activity.
|
|
398
407
|
|
|
399
408
|
```json
|
|
400
409
|
select_promising_posts({
|