@sellable/mcp 0.1.143 → 0.1.145
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -6
- package/agents/post-find-leads-message-scout.md +44 -0
- package/agents/registry.json +2 -2
- package/agents/source-scout-linkedin-engagement.md +4 -3
- package/agents/source-scout-prospeo-contact.md +1 -1
- package/agents/source-scout-sales-nav.md +3 -2
- package/dist/index-dev.js +0 -0
- package/dist/index.js +0 -0
- package/dist/tools/leads.js +168 -14
- package/dist/tools/processing.d.ts +1 -0
- package/dist/tools/prompts.js +3 -3
- package/dist/tools/rubrics.js +14 -9
- package/package.json +1 -1
- package/skills/create-campaign/SKILL.md +55 -34
- package/skills/create-campaign-v2/SKILL.md +59 -9
- package/skills/create-campaign-v2/SOUL.md +20 -12
- package/skills/create-campaign-v2/core/auto-execute.README.md +4 -4
- package/skills/create-campaign-v2/core/auto-execute.yaml +4 -4
- package/skills/create-campaign-v2/core/flow.v2.json +55 -19
- package/skills/create-campaign-v2/references/approval-gate-framing.md +1 -1
- package/skills/create-campaign-v2/references/filter-leads.md +42 -0
- package/skills/create-campaign-v2/references/final-handoff-contract.md +5 -5
- package/skills/create-campaign-v2/references/message-review-safety-gate.md +88 -13
- package/skills/create-campaign-v2/references/sample-validation-loop.md +18 -15
- package/skills/create-campaign-v2/references/step-13-import-leads.md +9 -6
- package/skills/create-campaign-v2/references/step-15-re-cascade.md +2 -3
- package/skills/create-campaign-v2/references/watch-guide-narration.md +39 -24
- package/skills/create-campaign-v2/references/watch-link-handoff.md +1 -1
- package/skills/create-campaign-v2-tail/SKILL.md +26 -13
- package/skills/load-voice/SKILL.md +129 -0
- package/skills/providers/prospeo.md +2 -1
- package/skills/providers/sales-nav.md +4 -2
- package/skills/providers/signal-discovery.md +12 -10
- package/skills/research/config.json +9 -0
package/README.md
CHANGED
|
@@ -16,10 +16,11 @@ Each message gets 5+ minutes of Claude attention with deep research - no other t
|
|
|
16
16
|
|
|
17
17
|
### Prompt Source Of Truth
|
|
18
18
|
|
|
19
|
-
There are
|
|
19
|
+
There are three public Sellable entrypoints shared across hosts:
|
|
20
20
|
|
|
21
21
|
- `sellable:create-campaign`
|
|
22
22
|
- `sellable:interview`
|
|
23
|
+
- `sellable:load-voice`
|
|
23
24
|
|
|
24
25
|
The create-campaign public wrapper at
|
|
25
26
|
`mcp/sellable/skills/create-campaign/SKILL.md` handles auth/bootstrap and loads
|
|
@@ -32,6 +33,11 @@ from:
|
|
|
32
33
|
|
|
33
34
|
- `mcp/sellable/skills/interview/SKILL.md`
|
|
34
35
|
|
|
36
|
+
The load-voice public wrapper loads the current voice/company memory for use in
|
|
37
|
+
writing, application answers, posts, replies, and reviews from:
|
|
38
|
+
|
|
39
|
+
- `mcp/sellable/skills/load-voice/SKILL.md`
|
|
40
|
+
|
|
35
41
|
Keep `create-campaign-v2` internal. Do not advertise it as a public command.
|
|
36
42
|
|
|
37
43
|
### 1. One-Command Agent Install
|
|
@@ -127,8 +133,9 @@ The installer does the full local setup:
|
|
|
127
133
|
`mcp__sellable__*` tools into skill sessions
|
|
128
134
|
|
|
129
135
|
After the installer passes, fully quit and reopen Codex Desktop. Start a new
|
|
130
|
-
thread and select `Sellable Create Campaign
|
|
131
|
-
invoke `$sellable:create-campaign
|
|
136
|
+
thread and select `Sellable Create Campaign`, `Sellable Identity Interview`, or
|
|
137
|
+
`Sellable Load Voice`; or invoke `$sellable:create-campaign`,
|
|
138
|
+
`$sellable:interview`, or `$sellable:load-voice`. If the app still says
|
|
132
139
|
`mcp__sellable__*` tools are missing after the installer passes, check that
|
|
133
140
|
`~/.codex/config.toml` contains both `[marketplaces.sellable]` and
|
|
134
141
|
`[plugins."sellable@sellable"]`.
|
|
@@ -141,13 +148,17 @@ Use these names consistently:
|
|
|
141
148
|
|
|
142
149
|
- Claude Code command: `/sellable:create-campaign`
|
|
143
150
|
- Claude Code command: `/sellable:interview`
|
|
151
|
+
- Claude Code command: `/sellable:load-voice`
|
|
144
152
|
- Codex command: `$sellable:create-campaign`
|
|
145
153
|
- Codex command: `$sellable:interview`
|
|
154
|
+
- Codex command: `$sellable:load-voice`
|
|
146
155
|
- Codex Desktop plugin: `sellable@sellable`
|
|
147
156
|
- Codex visible skill: `Sellable Create Campaign`
|
|
148
157
|
- Codex visible skill: `Sellable Identity Interview`
|
|
158
|
+
- Codex visible skill: `Sellable Load Voice`
|
|
149
159
|
- Codex skill frontmatter name: `create-campaign`
|
|
150
160
|
- Codex skill frontmatter name: `interview`
|
|
161
|
+
- Codex skill frontmatter name: `load-voice`
|
|
151
162
|
- MCP server name: `sellable`
|
|
152
163
|
- Internal workflow prompt: `create-campaign-v2`
|
|
153
164
|
|
|
@@ -255,9 +266,11 @@ Parallel execution contract:
|
|
|
255
266
|
through `get_source_scout_registry`. Add new scouts there first; installer,
|
|
256
267
|
Codex config, Claude agent files, and prompts should consume the registry.
|
|
257
268
|
- Post-find-leads scout names also come from `agents/registry.json` and are
|
|
258
|
-
exposed through `get_post_find_leads_scout_registry`. After
|
|
259
|
-
|
|
260
|
-
scout
|
|
269
|
+
exposed through `get_post_find_leads_scout_registry`. After
|
|
270
|
+
`confirm_lead_list` imports a non-empty review batch and rows are proven, use
|
|
271
|
+
that registry to launch the message-generation scout immediately when the host
|
|
272
|
+
supports real subagents. Launch the filter-leads scout later, only after the
|
|
273
|
+
user chooses filters.
|
|
261
274
|
- Claude host: use the installed `source-scout-linkedin-engagement`,
|
|
262
275
|
`source-scout-sales-nav`, and `source-scout-prospeo-contact` Task/Agent
|
|
263
276
|
subagents for parallel lead-source scouting only when the current session
|
|
@@ -136,3 +136,47 @@ Return a concise status with:
|
|
|
136
136
|
- one rendered omit/fallback sample
|
|
137
137
|
- quality-gate pass/fail summary
|
|
138
138
|
- whether final template review is ready or needs revision
|
|
139
|
+
|
|
140
|
+
When the parent will show the recommendation in chat, format the customer-facing
|
|
141
|
+
message review as Markdown with distinct copy blocks:
|
|
142
|
+
|
|
143
|
+
````markdown
|
|
144
|
+
## Message Template
|
|
145
|
+
|
|
146
|
+
**Subject**
|
|
147
|
+
|
|
148
|
+
```text
|
|
149
|
+
{{tokenized_subject}}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Body**
|
|
153
|
+
|
|
154
|
+
```text
|
|
155
|
+
{{tokenized_message_body}}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Rendered Examples
|
|
159
|
+
|
|
160
|
+
### Good token fill
|
|
161
|
+
|
|
162
|
+
```text
|
|
163
|
+
Subject: ...
|
|
164
|
+
|
|
165
|
+
Hey First,
|
|
166
|
+
|
|
167
|
+
...
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Good omit / fallback
|
|
171
|
+
|
|
172
|
+
```text
|
|
173
|
+
Subject: ...
|
|
174
|
+
|
|
175
|
+
Hey First,
|
|
176
|
+
|
|
177
|
+
...
|
|
178
|
+
```
|
|
179
|
+
````
|
|
180
|
+
|
|
181
|
+
Use a table for token rules. Keep explanations outside the fenced `text` blocks
|
|
182
|
+
so the user can quickly see exactly what copy is being approved.
|
package/agents/registry.json
CHANGED
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
"optionalProducesArtifacts": [],
|
|
169
169
|
"ownership": "lead quality, false-positive patterns, keep/exclude rules, ability-to-pay checks, and production rubric translation only",
|
|
170
170
|
"codex": {
|
|
171
|
-
"description": "Lead Fit Builder for campaign-backed lead filtering and rubric persistence after
|
|
171
|
+
"description": "Lead Fit Builder for campaign-backed lead filtering and rubric persistence after review-batch import and filter approval.",
|
|
172
172
|
"model": "gpt-5.5",
|
|
173
173
|
"modelReasoningEffort": "high",
|
|
174
174
|
"sandboxMode": "read-only",
|
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
]
|
|
180
180
|
},
|
|
181
181
|
"claude": {
|
|
182
|
-
"description": "Use proactively as Lead Fit Builder after
|
|
182
|
+
"description": "Use proactively as Lead Fit Builder after review-batch import and filter approval to persist campaign rubrics from campaign state.",
|
|
183
183
|
"model": "inherit",
|
|
184
184
|
"background": true,
|
|
185
185
|
"maxTurns": 8,
|
|
@@ -51,7 +51,8 @@ currentStep: "signal-discovery" })` before sampling so the watched Signal
|
|
|
51
51
|
visible headline/display-name cues only. Do not enrich people during
|
|
52
52
|
viability estimation.
|
|
53
53
|
7. Compute capacity before recommending the source: source target good-fit
|
|
54
|
-
leads (default
|
|
54
|
+
leads (default 150 for Signal Discovery unless the parent supplies a target),
|
|
55
|
+
reachable engagers,
|
|
55
56
|
sampled ICP-fit rate as `n/N` plus an easy percentage/range, expected usable
|
|
56
57
|
leads per 100 engagers before and after a conservative dedupe/cleanup
|
|
57
58
|
factor, required engagers to scrape (`source target / fit rate`), average
|
|
@@ -93,8 +94,8 @@ Evidence standards:
|
|
|
93
94
|
exist, how many sampled engagers looked in-ICP, how many good-fit prospects
|
|
94
95
|
that implies per 100 engagers, how many usable prospects one right-content
|
|
95
96
|
post should yield after cleanup, how many engagers must be scraped for the
|
|
96
|
-
|
|
97
|
+
150-good-fit source target, how many posts are needed for that source target,
|
|
97
98
|
and which posts you would use. Also say the first campaign import remains the
|
|
98
|
-
bounded
|
|
99
|
+
bounded review batch.
|
|
99
100
|
- If `fetch_post_engagers` is unavailable or fails, report that explicitly and mark the estimate lower-confidence.
|
|
100
101
|
- 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.
|
|
@@ -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.
|
|
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.
|
|
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:
|
|
@@ -28,8 +28,9 @@ Process:
|
|
|
28
28
|
2. Preserve target role names with `CURRENT_TITLE` lookups; do not rely on seniority alone when the brief names concrete roles.
|
|
29
29
|
3. When `lookup_sales_nav_filter` returns multiple title options, choose the closest semantic title match instead of the first result.
|
|
30
30
|
4. Build a broad-but-reasonable baseline from role/title, geography, company size, industry/account context, and recent LinkedIn activity when relevant.
|
|
31
|
-
5. Check scale against the source target good-fit lead count (default 300
|
|
32
|
-
unless the parent supplies a target).
|
|
31
|
+
5. Check scale against the source target good-fit lead count (default 300+
|
|
32
|
+
for Sales Nav, capped at 2,500 source candidates unless the parent supplies a target).
|
|
33
|
+
If raw preview volume or projected usable volume
|
|
33
34
|
is below target, do not present the tiny result as the scale fallback yet.
|
|
34
35
|
Loosen nonessential filters in order: remove recent-activity first, widen
|
|
35
36
|
adjacent title variants, widen geography/company-size constraints, and only
|
package/dist/index-dev.js
CHANGED
|
File without changes
|
package/dist/index.js
CHANGED
|
File without changes
|
package/dist/tools/leads.js
CHANGED
|
@@ -14,10 +14,11 @@ const workspaceRoot = resolveWorkspaceRoot(entryDir);
|
|
|
14
14
|
const skillsRoot = resolveSkillsDir();
|
|
15
15
|
const signalProviderConfigPath = join(skillsRoot, "create-campaign/core/providers/signal-discovery.json");
|
|
16
16
|
const leadImportLimitsPath = join(workspaceRoot, "lead-import-limits.json");
|
|
17
|
+
const campaignSourceDefaultsPath = join(workspaceRoot, "campaign-source-defaults.json");
|
|
17
18
|
const defaultLeadImportLimits = {
|
|
18
19
|
apollo: { maxImportCount: 2500 },
|
|
19
20
|
"sales-nav": { maxImportCount: 2500 },
|
|
20
|
-
prospeo: { maxImportCount:
|
|
21
|
+
prospeo: { maxImportCount: 2500 },
|
|
21
22
|
"signal-discovery": { maxImportCount: 2500 },
|
|
22
23
|
};
|
|
23
24
|
const defaultSignalDiscoveryConfig = {
|
|
@@ -25,6 +26,28 @@ const defaultSignalDiscoveryConfig = {
|
|
|
25
26
|
promisingPostsTarget: 3,
|
|
26
27
|
},
|
|
27
28
|
};
|
|
29
|
+
const defaultCampaignSourceDefaults = {
|
|
30
|
+
reviewBatch: {
|
|
31
|
+
defaultSize: 25,
|
|
32
|
+
minProjectedPass: 5,
|
|
33
|
+
},
|
|
34
|
+
providers: {
|
|
35
|
+
"signal-discovery": {
|
|
36
|
+
targetGoodFitLeads: 150,
|
|
37
|
+
defaultFitRate: 0.15,
|
|
38
|
+
maxSourceCandidates: 2500,
|
|
39
|
+
postCoverageBuffer: 1.2,
|
|
40
|
+
},
|
|
41
|
+
"sales-nav": {
|
|
42
|
+
targetGoodFitLeads: 300,
|
|
43
|
+
maxSourceCandidates: 2500,
|
|
44
|
+
},
|
|
45
|
+
prospeo: {
|
|
46
|
+
targetGoodFitLeads: 300,
|
|
47
|
+
maxSourceCandidates: 2500,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
28
51
|
const prospeoFilterValueSchema = {
|
|
29
52
|
type: "object",
|
|
30
53
|
description: "Include/exclude list filter (values must match Prospeo enums)",
|
|
@@ -245,6 +268,39 @@ function loadLeadImportLimits() {
|
|
|
245
268
|
return defaultLeadImportLimits;
|
|
246
269
|
}
|
|
247
270
|
}
|
|
271
|
+
function loadCampaignSourceDefaults() {
|
|
272
|
+
if (!existsSync(campaignSourceDefaultsPath)) {
|
|
273
|
+
return defaultCampaignSourceDefaults;
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
const raw = readFileSync(campaignSourceDefaultsPath, "utf-8");
|
|
277
|
+
const parsed = JSON.parse(raw);
|
|
278
|
+
return {
|
|
279
|
+
reviewBatch: {
|
|
280
|
+
...defaultCampaignSourceDefaults.reviewBatch,
|
|
281
|
+
...(parsed.reviewBatch ?? {}),
|
|
282
|
+
},
|
|
283
|
+
providers: {
|
|
284
|
+
...defaultCampaignSourceDefaults.providers,
|
|
285
|
+
...(parsed.providers ?? {}),
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return defaultCampaignSourceDefaults;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function getSignalDiscoverySourcePlanDefaults() {
|
|
294
|
+
const defaults = loadCampaignSourceDefaults();
|
|
295
|
+
const signalDefaults = defaults.providers["signal-discovery"];
|
|
296
|
+
const sourceCandidateTarget = Math.min(Math.ceil(signalDefaults.targetGoodFitLeads / signalDefaults.defaultFitRate), signalDefaults.maxSourceCandidates);
|
|
297
|
+
return {
|
|
298
|
+
targetGoodFitLeads: signalDefaults.targetGoodFitLeads,
|
|
299
|
+
defaultFitRate: signalDefaults.defaultFitRate,
|
|
300
|
+
sourceCandidateTarget,
|
|
301
|
+
reviewBatchSize: defaults.reviewBatch.defaultSize,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
248
304
|
function getMaxImportCount(provider) {
|
|
249
305
|
const limits = loadLeadImportLimits();
|
|
250
306
|
const max = limits[provider]?.maxImportCount;
|
|
@@ -443,17 +499,17 @@ function buildSourceImportWatchNarration({ provider, selectedPostCount, estimate
|
|
|
443
499
|
: ""}`
|
|
444
500
|
: `the approved ${providerLabel} source`;
|
|
445
501
|
const targetDetail = typeof targetLeadCount === "number"
|
|
446
|
-
? ` Targeting ${targetLeadCount.toLocaleString("en-US")} source
|
|
502
|
+
? ` Targeting ${targetLeadCount.toLocaleString("en-US")} source candidates before the bounded review batch is cloned.`
|
|
447
503
|
: "";
|
|
448
504
|
return {
|
|
449
505
|
stage: "review-batch",
|
|
450
506
|
headline: provider === "signal-discovery"
|
|
451
|
-
? "Scraping source
|
|
452
|
-
: "Importing source
|
|
507
|
+
? "Scraping source candidates from posts"
|
|
508
|
+
: "Importing source candidates",
|
|
453
509
|
visibleState: `The browser is showing ${providerLabel} import progress for ${sourceDetail}.${targetDetail}`,
|
|
454
510
|
agentIntent: "Codex is materializing the approved source into a lead list before cloning only the bounded review batch into the campaign.",
|
|
455
|
-
nextAction: "Wait for source
|
|
456
|
-
safety: "
|
|
511
|
+
nextAction: "Wait for source candidates, then import the review batch",
|
|
512
|
+
safety: "Import is limited to the review batch.",
|
|
457
513
|
};
|
|
458
514
|
}
|
|
459
515
|
function buildSourceImportRecoveryWatchNarration(args) {
|
|
@@ -481,7 +537,7 @@ function buildSourceImportRecoveryWatchNarration(args) {
|
|
|
481
537
|
nextAction: args.reason === "failed" || args.reason === "zero"
|
|
482
538
|
? "Retry the import or change the approved source"
|
|
483
539
|
: "Wait again, retry readiness, or change the source",
|
|
484
|
-
safety: "
|
|
540
|
+
safety: "The campaign is still at source recovery.",
|
|
485
541
|
};
|
|
486
542
|
}
|
|
487
543
|
function buildFilterChoiceWatchNarration({ sourceLeadCount, reviewRowCount, }) {
|
|
@@ -497,7 +553,7 @@ function buildFilterChoiceWatchNarration({ sourceLeadCount, reviewRowCount, }) {
|
|
|
497
553
|
visibleState: `${reviewCopy} The browser is showing the filter-choice screen and sample rows.`,
|
|
498
554
|
agentIntent: "Codex is pausing on this sample because a mixed review list should be filtered before message review. Skip filters only if the visible rows already look clean.",
|
|
499
555
|
nextAction: "Choose filters or skip",
|
|
500
|
-
safety: "
|
|
556
|
+
safety: "This choice sets the filter path.",
|
|
501
557
|
workerStatuses: {
|
|
502
558
|
leadFitBuilder: "idle",
|
|
503
559
|
messageDraftBuilder: "idle",
|
|
@@ -511,7 +567,7 @@ function buildSignalDiscoverySearchWatchNarration() {
|
|
|
511
567
|
visibleState: "The browser is showing Signal Discovery while LinkedIn post searches run.",
|
|
512
568
|
agentIntent: "Codex is searching the approved post themes and will ask before scraping selected engagers into a source list.",
|
|
513
569
|
nextAction: "Review selected posts before scraping",
|
|
514
|
-
safety: "
|
|
570
|
+
safety: "Scrape approval is the next gate.",
|
|
515
571
|
};
|
|
516
572
|
}
|
|
517
573
|
function buildSignalDiscoveryResultsWatchNarration(postsReturned) {
|
|
@@ -524,7 +580,7 @@ function buildSignalDiscoveryResultsWatchNarration(postsReturned) {
|
|
|
524
580
|
visibleState: postCopy,
|
|
525
581
|
agentIntent: "Codex is promoting the strongest posts, then it will ask before scraping the selected engager pool.",
|
|
526
582
|
nextAction: "Approve selected posts before scraping",
|
|
527
|
-
safety: "
|
|
583
|
+
safety: "Scrape approval is the next gate.",
|
|
528
584
|
};
|
|
529
585
|
}
|
|
530
586
|
function buildSelectedPostApprovalWatchNarration(selectedPostCount) {
|
|
@@ -533,10 +589,59 @@ function buildSelectedPostApprovalWatchNarration(selectedPostCount) {
|
|
|
533
589
|
headline: "Approve selected-post scrape",
|
|
534
590
|
visibleState: `${selectedPostCount.toLocaleString("en-US")} LinkedIn post${selectedPostCount === 1 ? "" : "s"} selected in Signal Discovery.`,
|
|
535
591
|
agentIntent: "Codex is asking before scraping this selected engager pool into a source list and cloning only the bounded review batch into the campaign.",
|
|
536
|
-
nextAction: `
|
|
537
|
-
safety: "
|
|
592
|
+
nextAction: `Approve scraping ${selectedPostCount.toLocaleString("en-US")} Signal Discovery post${selectedPostCount === 1 ? "" : "s"}`,
|
|
593
|
+
safety: "Scrape approval is the next gate.",
|
|
538
594
|
};
|
|
539
595
|
}
|
|
596
|
+
function escapeMarkdownTableCell(value) {
|
|
597
|
+
return (value || "-").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
|
|
598
|
+
}
|
|
599
|
+
function formatApproxInteger(value) {
|
|
600
|
+
const rounded = value >= 100 ? Math.round(value / 10) * 10 : Math.round(value);
|
|
601
|
+
return `~${rounded.toLocaleString("en-US")}`;
|
|
602
|
+
}
|
|
603
|
+
function buildSignalDiscoverySourceRecommendation({ selectedPosts, }) {
|
|
604
|
+
const { targetGoodFitLeads, defaultFitRate, sourceCandidateTarget, reviewBatchSize, } = getSignalDiscoverySourcePlanDefaults();
|
|
605
|
+
const selectedCount = selectedPosts.length;
|
|
606
|
+
const totalEngagement = selectedPosts.reduce((sum, post) => sum + (post.likes ?? 0) + (post.comments ?? 0), 0);
|
|
607
|
+
const tableRows = selectedPosts
|
|
608
|
+
.map((post) => {
|
|
609
|
+
const engagement = (post.likes ?? 0) + (post.comments ?? 0);
|
|
610
|
+
return `| ${escapeMarkdownTableCell(post.authorName)} | ${escapeMarkdownTableCell(post.reason)} | ${formatApproxInteger(engagement)} |`;
|
|
611
|
+
})
|
|
612
|
+
.join("\n");
|
|
613
|
+
const estimatedGoodFit = totalEngagement * defaultFitRate;
|
|
614
|
+
return `## Source Recommendation
|
|
615
|
+
|
|
616
|
+
Use Signal Discovery first.
|
|
617
|
+
|
|
618
|
+
**Good-fit target:** ~${targetGoodFitLeads.toLocaleString("en-US")} prospects after cleanup, enrichment, and filters<br>
|
|
619
|
+
**Source-candidate plan:** scrape ~${sourceCandidateTarget.toLocaleString("en-US")} raw engagers using a conservative ${Math.round(defaultFitRate * 100)}% fit-rate assumption<br>
|
|
620
|
+
**Review checkpoint:** import the first ${reviewBatchSize.toLocaleString("en-US")} leads into the campaign for fit and message review before scaling
|
|
621
|
+
|
|
622
|
+
### Selected posts
|
|
623
|
+
|
|
624
|
+
| Post | Why it fits | Visible engagement |
|
|
625
|
+
|---|---|---:|
|
|
626
|
+
${tableRows || "| Selected posts | Campaign-matched public engagement | - |"}
|
|
627
|
+
|
|
628
|
+
**Total visible pool:** ${formatApproxInteger(totalEngagement)} engagers<br>
|
|
629
|
+
**Estimated good-fit pool at ${Math.round(defaultFitRate * 100)}%:** ${formatApproxInteger(estimatedGoodFit)} prospects before dedupe/risk cleanup
|
|
630
|
+
|
|
631
|
+
### Recommendation
|
|
632
|
+
|
|
633
|
+
Approve scraping these ${selectedCount} posts.
|
|
634
|
+
|
|
635
|
+
This gives enough volume to work toward ~${targetGoodFitLeads.toLocaleString("en-US")} good-fit prospects while keeping the source tied to people already engaging with the campaign's strongest public buying signals.
|
|
636
|
+
|
|
637
|
+
**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.
|
|
638
|
+
|
|
639
|
+
**Fallback:** if the review batch is too vendor-heavy, agency-heavy, or off-ICP, switch to Sales Nav recent activity.
|
|
640
|
+
|
|
641
|
+
Approval card should say:
|
|
642
|
+
|
|
643
|
+
**Approve scraping ${selectedCount} Signal Discovery post${selectedCount === 1 ? "" : "s"}?**`;
|
|
644
|
+
}
|
|
540
645
|
function normalizeImportProvider(provider) {
|
|
541
646
|
if (provider === "apollo-ai" || provider === "apollo")
|
|
542
647
|
return "apollo";
|
|
@@ -1117,7 +1222,7 @@ export const leadToolDefinitions = [
|
|
|
1117
1222
|
},
|
|
1118
1223
|
targetEngagerCount: {
|
|
1119
1224
|
type: "number",
|
|
1120
|
-
description: "Signal Discovery: target number of post engagers to scrape. Compute from source target good-fit leads / sampled fit rate; e.g.
|
|
1225
|
+
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.",
|
|
1121
1226
|
},
|
|
1122
1227
|
maxPostsToScrape: {
|
|
1123
1228
|
type: "number",
|
|
@@ -2726,12 +2831,61 @@ export async function selectPromisingPosts(input) {
|
|
|
2726
2831
|
currentStep: "signal-discovery",
|
|
2727
2832
|
watchNarration: buildSelectedPostApprovalWatchNarration(selectionResult.selectedCount),
|
|
2728
2833
|
});
|
|
2834
|
+
let sourceRecommendation = "";
|
|
2835
|
+
try {
|
|
2836
|
+
const tabsResponse = await api.get(`/api/v3/campaigns/${campaignOfferId}/signal-discovery/tabs`);
|
|
2837
|
+
const reasonsByPostId = new Map(selections.map((selection) => [selection.postId, selection.reason]));
|
|
2838
|
+
const selectedByUrl = new Map();
|
|
2839
|
+
for (const tab of tabsResponse.tabs ?? []) {
|
|
2840
|
+
for (const post of tab.posts ?? []) {
|
|
2841
|
+
if (!post.isSelected)
|
|
2842
|
+
continue;
|
|
2843
|
+
const urlKey = normalizePostUrl(post.postUrl ?? undefined) ||
|
|
2844
|
+
post.postUrl ||
|
|
2845
|
+
post.id ||
|
|
2846
|
+
"";
|
|
2847
|
+
if (!urlKey)
|
|
2848
|
+
continue;
|
|
2849
|
+
const directReason = post.id ? reasonsByPostId.get(post.id) : undefined;
|
|
2850
|
+
const urlReason = post.postUrl && reasonsByPostId.get(post.postUrl)
|
|
2851
|
+
? reasonsByPostId.get(post.postUrl)
|
|
2852
|
+
: undefined;
|
|
2853
|
+
selectedByUrl.set(urlKey, {
|
|
2854
|
+
authorName: post.authorName,
|
|
2855
|
+
reason: directReason ||
|
|
2856
|
+
urlReason ||
|
|
2857
|
+
(post.postContent ? truncate(post.postContent, 70) : null),
|
|
2858
|
+
likes: post.likes,
|
|
2859
|
+
comments: post.comments,
|
|
2860
|
+
});
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
sourceRecommendation = buildSignalDiscoverySourceRecommendation({
|
|
2864
|
+
selectedPosts: Array.from(selectedByUrl.values()),
|
|
2865
|
+
});
|
|
2866
|
+
}
|
|
2867
|
+
catch {
|
|
2868
|
+
const { reviewBatchSize } = getSignalDiscoverySourcePlanDefaults();
|
|
2869
|
+
sourceRecommendation = `## Source Recommendation
|
|
2870
|
+
|
|
2871
|
+
Use Signal Discovery first.
|
|
2872
|
+
|
|
2873
|
+
**Recommendation:** approve scraping the ${selectionResult.selectedCount} selected Signal Discovery post${selectionResult.selectedCount === 1 ? "" : "s"}.
|
|
2874
|
+
|
|
2875
|
+
**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.
|
|
2876
|
+
|
|
2877
|
+
Approval card should say:
|
|
2878
|
+
|
|
2879
|
+
**Approve scraping ${selectionResult.selectedCount} Signal Discovery post${selectionResult.selectedCount === 1 ? "" : "s"}?**`;
|
|
2880
|
+
}
|
|
2729
2881
|
return {
|
|
2730
2882
|
success: true,
|
|
2731
2883
|
selectedCount: selectionResult.selectedCount,
|
|
2732
2884
|
unselectedCount: selectionResult.unselectedCount,
|
|
2733
2885
|
criteriaCount: selectionResult.criteriaCount,
|
|
2734
|
-
message:
|
|
2886
|
+
message: `${sourceRecommendation}
|
|
2887
|
+
|
|
2888
|
+
Selected ${selectionResult.selectedCount} posts with ${selectionResult.criteriaCount} ICP criteria persisted. Ask the user to approve the specific scraping action before calling import_leads; the earlier source-plan approval only authorized search/scouting.`,
|
|
2735
2889
|
};
|
|
2736
2890
|
}
|
|
2737
2891
|
export async function setHeadlineICPCriteria(input) {
|
|
@@ -60,6 +60,7 @@ export type CampaignRubricsResponse = {
|
|
|
60
60
|
enableICPFilters: boolean | null;
|
|
61
61
|
currentStep?: string | null;
|
|
62
62
|
workflowTableId: string | null;
|
|
63
|
+
hasApprovedMessageTemplate?: boolean | null;
|
|
63
64
|
rubrics: RubricItem[];
|
|
64
65
|
};
|
|
65
66
|
export declare function fetchCampaignRubrics(campaignOfferId: string): Promise<CampaignRubricsResponse>;
|
package/dist/tools/prompts.js
CHANGED
|
@@ -339,9 +339,9 @@ export function getPostFindLeadsScoutRegistry() {
|
|
|
339
339
|
reusePolicy: "The first completed Message Draft Builder recommendation remains the default review candidate. Later Lead Fit Builder, Filter Leads, enrichment, or rubric completion may make an enriched rewrite available, but does not automatically retry or replace the initial draft unless campaign/brief/source/list/table/review-batch identity mismatches or the initial output failed.",
|
|
340
340
|
},
|
|
341
341
|
usage: {
|
|
342
|
-
codex: "After
|
|
343
|
-
claude: "After
|
|
344
|
-
parentThreadRule: "Named agents are optional acceleration. If they are absent, do not customer-surface install status; the main thread still orchestrates filter and message branches from CampaignOffer state, selected source state, workflowTableId, and imported review-batch rows. Local markdown/json files are not normal-path inputs. The message drafting branch must load the full generate-messages prompt, must read live campaign/review-batch state through scoped MCP/product tools, and must reject mismatched selectedLeadListId/workflowTableId/campaign/workspace input. Template approval in the parent thread uses the create-campaign-v2 message-review safety gate; do not load the full generate-messages prompt for approval-only review. Join before message review. 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.",
|
|
342
|
+
codex: "After confirm_lead_list imports a non-empty bounded review batch and get_rows_minimal proves rows for workflowTableId, spawn the returned Message Draft Builder scout name immediately when the current Codex host exposes that custom agent. Spawn the Lead Fit Builder scout only after the user chooses filters.",
|
|
343
|
+
claude: "After confirm_lead_list imports a non-empty bounded review batch and get_rows_minimal proves rows for workflowTableId, invoke the returned Message Draft Builder Task/Agent immediately when the current Claude session lists that agent. Invoke the Lead Fit Builder only after the user chooses filters.",
|
|
344
|
+
parentThreadRule: "Named agents are optional acceleration. If they are absent, do not customer-surface install status; the main thread still orchestrates filter and message branches from CampaignOffer state, selected source state, workflowTableId, and imported review-batch rows. Local markdown/json files are not normal-path inputs. The message drafting branch starts at the review-batch import gate, must load the full generate-messages prompt, must read live campaign/review-batch state through scoped MCP/product tools, and must reject mismatched selectedLeadListId/workflowTableId/campaign/workspace input. Template approval in the parent thread uses the create-campaign-v2 message-review safety gate; do not load the full generate-messages prompt for approval-only review. Join before message review. 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
345
|
},
|
|
346
346
|
};
|
|
347
347
|
}
|
package/dist/tools/rubrics.js
CHANGED
|
@@ -86,21 +86,26 @@ async function fetchCampaignOffer(campaignOfferId) {
|
|
|
86
86
|
workflowTableId: v3.workflowTableId,
|
|
87
87
|
enableICPFilters: v3.enableICPFilters,
|
|
88
88
|
currentStep: v3.currentStep,
|
|
89
|
+
hasApprovedMessageTemplate: v3.hasApprovedMessageTemplate,
|
|
89
90
|
leadScoringRubrics: v3.rubrics,
|
|
90
91
|
};
|
|
91
92
|
}
|
|
92
93
|
const filterLeadsReadyWatchNarration = {
|
|
93
94
|
stage: "fit-message",
|
|
94
|
-
headline: "Filter
|
|
95
|
-
visibleState: "
|
|
96
|
-
agentIntent: "Codex is
|
|
97
|
-
nextAction: "Review
|
|
95
|
+
headline: "Filter rules saved",
|
|
96
|
+
visibleState: "The browser is staying on Filter Leads with the fit rules saved.",
|
|
97
|
+
agentIntent: "Codex is preparing the message template for approval before enrichment, filtering, or message cells run.",
|
|
98
|
+
nextAction: "Review the message template in chat",
|
|
98
99
|
progressLabel: "Fit + message",
|
|
99
|
-
safety: "
|
|
100
|
+
safety: "Template approval comes before row execution.",
|
|
100
101
|
};
|
|
101
|
-
function shouldMoveToFilterLeads(
|
|
102
|
+
function shouldMoveToFilterLeads(campaign) {
|
|
103
|
+
const currentStep = campaign.currentStep;
|
|
102
104
|
if (!currentStep)
|
|
103
105
|
return true;
|
|
106
|
+
if (currentStep === "messages") {
|
|
107
|
+
return campaign.hasApprovedMessageTemplate === false;
|
|
108
|
+
}
|
|
104
109
|
return [
|
|
105
110
|
"confirm-lead-list",
|
|
106
111
|
"filter-choice",
|
|
@@ -109,11 +114,11 @@ function shouldMoveToFilterLeads(currentStep) {
|
|
|
109
114
|
"apply-icp-rubric",
|
|
110
115
|
].includes(currentStep);
|
|
111
116
|
}
|
|
112
|
-
function buildEnableIcpFiltersPayload(
|
|
117
|
+
function buildEnableIcpFiltersPayload(campaign) {
|
|
113
118
|
const payload = {
|
|
114
119
|
enableICPFilters: true,
|
|
115
120
|
};
|
|
116
|
-
if (shouldMoveToFilterLeads(
|
|
121
|
+
if (shouldMoveToFilterLeads(campaign)) {
|
|
117
122
|
payload.currentStep = "apply-icp-rubric";
|
|
118
123
|
payload.watchNarration = filterLeadsReadyWatchNarration;
|
|
119
124
|
}
|
|
@@ -568,7 +573,7 @@ export async function saveRubrics(input) {
|
|
|
568
573
|
let currentStepSet = false;
|
|
569
574
|
try {
|
|
570
575
|
const api = getApi();
|
|
571
|
-
const payload = buildEnableIcpFiltersPayload(campaign
|
|
576
|
+
const payload = buildEnableIcpFiltersPayload(campaign);
|
|
572
577
|
await api.put(`/api/v2/campaign-offers/${input.campaignOfferId}`, payload);
|
|
573
578
|
enableICPFiltersSet = true;
|
|
574
579
|
currentStepSet = payload.currentStep === "apply-icp-rubric";
|