@sellable/mcp 0.1.112 → 0.1.114
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 +11 -3
- package/dist/tools/campaigns.js +3 -0
- package/dist/tools/context.d.ts +2 -1
- package/dist/tools/context.js +1 -0
- package/dist/tools/leads.js +1 -1
- package/dist/tools/linkedin.js +1 -1
- package/dist/tools/navigation.d.ts +4 -2
- package/dist/tools/navigation.js +13 -4
- package/dist/tools/readiness.d.ts +6 -3
- package/dist/tools/rubrics.js +8 -6
- package/dist/tools/watch-url-security.js +4 -0
- package/package.json +1 -1
- package/skills/create-campaign/SKILL.md +30 -27
- package/skills/create-campaign-v2/SKILL.md +25 -4
- package/skills/create-campaign-v2/core/flow.v2.json +60 -17
- package/skills/create-campaign-v2/references/sample-validation-loop.md +20 -11
- package/skills/create-campaign-v2/references/watch-guide-narration.md +39 -3
- package/skills/create-campaign-v2/references/watch-link-handoff.md +12 -7
- package/skills/create-campaign-v2-tail/SKILL.md +24 -19
- package/skills/providers/signal-discovery.md +7 -0
|
@@ -17,14 +17,22 @@ Use the inherited Sellable MCP tools when available:
|
|
|
17
17
|
- `search_signals` to find recent post lanes. Include `campaignOfferId` whenever
|
|
18
18
|
the parent provides one so selected searches/lists stay attached to the
|
|
19
19
|
campaign.
|
|
20
|
-
- `
|
|
20
|
+
- `select_promising_posts` to promote the exact posts you will sample into the
|
|
21
|
+
campaign UI before fetching engagers. In campaign-attached runs, do this
|
|
22
|
+
before the first `fetch_post_engagers` call so the user can see which posts
|
|
23
|
+
are being sampled.
|
|
24
|
+
- `fetch_post_engagers` to sample engagers from promoted/selected posts.
|
|
21
25
|
|
|
22
26
|
Process:
|
|
23
27
|
|
|
24
28
|
1. Read the campaign brief, kickoff doc, or lane prompt supplied by the parent.
|
|
25
29
|
2. Search 3-5 keyword/topic lanes, favoring fresh posts from the last 7-14 days.
|
|
26
|
-
3. Select 3-5 promising posts when available.
|
|
27
|
-
|
|
30
|
+
3. Select 3-5 promising posts when available. If a `campaignOfferId` was
|
|
31
|
+
supplied, call `select_promising_posts({ campaignOfferId, selectionMode:
|
|
32
|
+
"replace", selections, headlineICPCriteria, currentStep:
|
|
33
|
+
"signal-discovery" })` before sampling so the watched Signal Discovery table
|
|
34
|
+
shows the promoted posts.
|
|
35
|
+
4. Fetch or sample engagers for promoted posts and score rough ICP fit from visible headline/display-name cues only. Do not enrich people during viability estimation.
|
|
28
36
|
5. Estimate usable prospects per selected post from sampled pass rate. If the sample is good but volume is low, say how many more similar posts should be added or scraped.
|
|
29
37
|
6. Return false positives and dead ends explicitly.
|
|
30
38
|
|
package/dist/tools/campaigns.js
CHANGED
|
@@ -25,6 +25,9 @@ export function buildWatchUrl(config, path) {
|
|
|
25
25
|
if (workspaceId && !url.searchParams.has("workspaceId")) {
|
|
26
26
|
url.searchParams.set("workspaceId", workspaceId);
|
|
27
27
|
}
|
|
28
|
+
if (config.token && !url.searchParams.has("token")) {
|
|
29
|
+
url.searchParams.set("token", config.token);
|
|
30
|
+
}
|
|
28
31
|
return url.toString();
|
|
29
32
|
}
|
|
30
33
|
export function getCampaignBuilderWatchModeParam() {
|
package/dist/tools/context.d.ts
CHANGED
package/dist/tools/context.js
CHANGED
package/dist/tools/leads.js
CHANGED
|
@@ -1047,7 +1047,7 @@ export const leadToolDefinitions = [
|
|
|
1047
1047
|
},
|
|
1048
1048
|
{
|
|
1049
1049
|
name: "select_promising_posts",
|
|
1050
|
-
description: "Select the most promising LinkedIn posts for lead scraping AND provide headline ICP criteria. Use the selectionTarget returned by search_signals (default 3).",
|
|
1050
|
+
description: "Select the most promising LinkedIn posts for lead scraping AND provide headline ICP criteria. Use the selectionTarget returned by search_signals (default 3). In campaign-attached sampling, call select_promising_posts first to promote the sampled posts into the watched UI before fetch_post_engagers.",
|
|
1051
1051
|
inputSchema: {
|
|
1052
1052
|
type: "object",
|
|
1053
1053
|
properties: {
|
package/dist/tools/linkedin.js
CHANGED
|
@@ -21,7 +21,7 @@ export const linkedinToolDefinitions = [
|
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
name: "fetch_post_engagers",
|
|
24
|
-
description: "Fetch people who engaged with a LinkedIn post (reactions and/or comments). Returns deduplicated engagers with name, headline, profileUrl, and source. Use 'comments' for lead magnets to get people who actively engaged. Uses smart rate limiting and parallel fetching.",
|
|
24
|
+
description: "Fetch people who engaged with a LinkedIn post (reactions and/or comments). Returns deduplicated engagers with name, headline, profileUrl, and source. Use 'comments' for lead magnets to get people who actively engaged. Uses smart rate limiting and parallel fetching. In campaign-attached Signal Discovery sampling, call select_promising_posts first to promote the sampled posts into the watched UI.",
|
|
25
25
|
inputSchema: {
|
|
26
26
|
type: "object",
|
|
27
27
|
properties: {
|
|
@@ -81,7 +81,8 @@ export declare function computeCampaignNavigationStateFromCampaign(campaign: Cam
|
|
|
81
81
|
};
|
|
82
82
|
watchUrl: {
|
|
83
83
|
urlPresent: boolean;
|
|
84
|
-
direct: boolean
|
|
84
|
+
direct: boolean;
|
|
85
|
+
autoLogin: boolean;
|
|
85
86
|
redirectPath: string | null;
|
|
86
87
|
};
|
|
87
88
|
table: {
|
|
@@ -125,7 +126,8 @@ export declare function getCampaignNavigationState(input: GetCampaignNavigationS
|
|
|
125
126
|
};
|
|
126
127
|
watchUrl: {
|
|
127
128
|
urlPresent: boolean;
|
|
128
|
-
direct: boolean
|
|
129
|
+
direct: boolean;
|
|
130
|
+
autoLogin: boolean;
|
|
129
131
|
redirectPath: string | null;
|
|
130
132
|
};
|
|
131
133
|
table: {
|
package/dist/tools/navigation.js
CHANGED
|
@@ -385,7 +385,9 @@ function parseWatchUrl(watchUrl, campaignId) {
|
|
|
385
385
|
if (!watchUrl) {
|
|
386
386
|
return {
|
|
387
387
|
urlPresent: false,
|
|
388
|
+
direct: false,
|
|
388
389
|
signed: false,
|
|
390
|
+
autoLogin: false,
|
|
389
391
|
redirectPath: null,
|
|
390
392
|
warning: null,
|
|
391
393
|
};
|
|
@@ -398,11 +400,15 @@ function parseWatchUrl(watchUrl, campaignId) {
|
|
|
398
400
|
url.searchParams.get("mode") === "codex" ||
|
|
399
401
|
url.searchParams.get("mode") === "watch");
|
|
400
402
|
if (isDirectCampaignUrl) {
|
|
403
|
+
const autoLogin = Boolean(url.searchParams.get("token"));
|
|
401
404
|
return {
|
|
402
405
|
urlPresent: true,
|
|
403
406
|
direct: true,
|
|
407
|
+
autoLogin,
|
|
404
408
|
redirectPath: `${url.pathname}?${url.searchParams.toString()}`.replace(/\?$/, ""),
|
|
405
|
-
warning:
|
|
409
|
+
warning: autoLogin
|
|
410
|
+
? null
|
|
411
|
+
: "Watch URL is direct but missing token auto-login. Recover a fresh /campaign-builder/{campaignId}?mode={claude|codex}&token=... watch link before asking the user to open it.",
|
|
406
412
|
};
|
|
407
413
|
}
|
|
408
414
|
const redirect = url.searchParams.get("redirect");
|
|
@@ -413,18 +419,20 @@ function parseWatchUrl(watchUrl, campaignId) {
|
|
|
413
419
|
return {
|
|
414
420
|
urlPresent: true,
|
|
415
421
|
direct: false,
|
|
422
|
+
autoLogin: false,
|
|
416
423
|
redirectPath: decodedRedirect,
|
|
417
424
|
warning: isLegacySigned
|
|
418
|
-
? "Watch URL still uses a tokenized /auth/continue handoff. Return the
|
|
419
|
-
: "Watch URL is not a
|
|
425
|
+
? "Watch URL still uses a tokenized /auth/continue handoff. Return the direct /campaign-builder/{campaignId}?mode={claude|codex}&token=... watch URL instead."
|
|
426
|
+
: "Watch URL is not a direct /campaign-builder/{campaignId}?mode={claude|codex}&token=... watch link.",
|
|
420
427
|
};
|
|
421
428
|
}
|
|
422
429
|
catch {
|
|
423
430
|
return {
|
|
424
431
|
urlPresent: true,
|
|
425
432
|
direct: false,
|
|
433
|
+
autoLogin: false,
|
|
426
434
|
redirectPath: null,
|
|
427
|
-
warning: "Watch URL is malformed; recover a fresh
|
|
435
|
+
warning: "Watch URL is malformed; recover a fresh direct campaign-builder auto-login watch link before handing it to the user.",
|
|
428
436
|
};
|
|
429
437
|
}
|
|
430
438
|
}
|
|
@@ -593,6 +601,7 @@ export function computeCampaignNavigationStateFromCampaign(campaign, options) {
|
|
|
593
601
|
watchUrl: {
|
|
594
602
|
urlPresent: watchUrlState.urlPresent,
|
|
595
603
|
direct: watchUrlState.direct,
|
|
604
|
+
autoLogin: watchUrlState.autoLogin,
|
|
596
605
|
redirectPath: watchUrlState.redirectPath,
|
|
597
606
|
},
|
|
598
607
|
table: {
|
|
@@ -134,7 +134,8 @@ export declare function waitForCampaignTableReady(input: WaitForCampaignTableRea
|
|
|
134
134
|
};
|
|
135
135
|
watchUrl: {
|
|
136
136
|
urlPresent: boolean;
|
|
137
|
-
direct: boolean
|
|
137
|
+
direct: boolean;
|
|
138
|
+
autoLogin: boolean;
|
|
138
139
|
redirectPath: string | null;
|
|
139
140
|
};
|
|
140
141
|
table: {
|
|
@@ -183,7 +184,8 @@ export declare function waitForCampaignTableReady(input: WaitForCampaignTableRea
|
|
|
183
184
|
};
|
|
184
185
|
watchUrl: {
|
|
185
186
|
urlPresent: boolean;
|
|
186
|
-
direct: boolean
|
|
187
|
+
direct: boolean;
|
|
188
|
+
autoLogin: boolean;
|
|
187
189
|
redirectPath: string | null;
|
|
188
190
|
};
|
|
189
191
|
table: {
|
|
@@ -234,7 +236,8 @@ export declare function waitForCampaignTableReady(input: WaitForCampaignTableRea
|
|
|
234
236
|
};
|
|
235
237
|
watchUrl: {
|
|
236
238
|
urlPresent: boolean;
|
|
237
|
-
direct: boolean
|
|
239
|
+
direct: boolean;
|
|
240
|
+
autoLogin: boolean;
|
|
238
241
|
redirectPath: string | null;
|
|
239
242
|
};
|
|
240
243
|
table: {
|
package/dist/tools/rubrics.js
CHANGED
|
@@ -204,7 +204,7 @@ export const rubricToolDefinitions = [
|
|
|
204
204
|
},
|
|
205
205
|
{
|
|
206
206
|
name: "save_rubrics",
|
|
207
|
-
description: "Persist rubric criteria to the campaign. Pass leadScoringRubrics directly to save without drafting.
|
|
207
|
+
description: "Persist rubric criteria to the campaign. Pass leadScoringRubrics directly to save without drafting. Saving active rubrics enables ICP filtering on the campaign so the client can continue to Filter Leads.",
|
|
208
208
|
inputSchema: {
|
|
209
209
|
type: "object",
|
|
210
210
|
properties: {
|
|
@@ -355,7 +355,7 @@ export const rubricToolDefinitions = [
|
|
|
355
355
|
},
|
|
356
356
|
minPassedCount: {
|
|
357
357
|
type: "number",
|
|
358
|
-
description: "Optional pass floor for bounded create-campaign samples. When this floor is met
|
|
358
|
+
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.",
|
|
359
359
|
},
|
|
360
360
|
timeoutMs: {
|
|
361
361
|
type: "number",
|
|
@@ -686,11 +686,13 @@ export async function waitForRubricResults(input) {
|
|
|
686
686
|
stats,
|
|
687
687
|
};
|
|
688
688
|
}
|
|
689
|
-
if (minPassFloorMet
|
|
689
|
+
if (minPassFloorMet) {
|
|
690
690
|
const percent = completed > 0 ? Math.round((passed / completed) * 100) : 0;
|
|
691
|
-
const reason =
|
|
692
|
-
? "
|
|
693
|
-
:
|
|
691
|
+
const reason = !noActiveProcessing
|
|
692
|
+
? "min_passed_count_met_with_active_processing"
|
|
693
|
+
: unresolvedRowsResolvedAsFailures
|
|
694
|
+
? "min_passed_count_met_with_resolved_failures"
|
|
695
|
+
: "min_passed_count_met_no_active_processing";
|
|
694
696
|
const rowSnapshot = includeRows
|
|
695
697
|
? await getTableRowsMinimal(tableId, {
|
|
696
698
|
limit: effectiveTarget,
|
|
@@ -6,9 +6,13 @@ function sanitizeWatchUrlValue(value) {
|
|
|
6
6
|
if (redirect) {
|
|
7
7
|
const direct = new URL(decodeURIComponent(redirect), url.origin);
|
|
8
8
|
const workspaceId = url.searchParams.get("workspaceId");
|
|
9
|
+
const token = url.searchParams.get("token");
|
|
9
10
|
if (workspaceId && !direct.searchParams.has("workspaceId")) {
|
|
10
11
|
direct.searchParams.set("workspaceId", workspaceId);
|
|
11
12
|
}
|
|
13
|
+
if (token && !direct.searchParams.has("token")) {
|
|
14
|
+
direct.searchParams.set("token", token);
|
|
15
|
+
}
|
|
12
16
|
return direct.toString();
|
|
13
17
|
}
|
|
14
18
|
}
|
package/package.json
CHANGED
|
@@ -134,6 +134,12 @@ claim they ran in parallel and do not surface install status to the customer. In
|
|
|
134
134
|
chat, call the downstream copy stage `message generation`;
|
|
135
135
|
`message-validation.md` is only an internal proof artifact.
|
|
136
136
|
|
|
137
|
+
For campaign-attached Signal Discovery sampling, promote/select the exact posts
|
|
138
|
+
with `select_promising_posts` before `fetch_post_engagers` so the user can see
|
|
139
|
+
which posts are being sampled in the watched app. The watch guide should say
|
|
140
|
+
that we are pulling sample engagers from these posts to confirm the ICP is
|
|
141
|
+
actually engaging and the source is viable.
|
|
142
|
+
|
|
137
143
|
After find-leads returns a lead source and the user approves it, use the same
|
|
138
144
|
registry pattern for the two post-lead branches. The create-campaign-v2 subskill
|
|
139
145
|
calls `get_post_find_leads_scout_registry`, then launches the returned
|
|
@@ -159,45 +165,42 @@ linked skill versions, runbooks, npm/package details, repo-local files, VPS or
|
|
|
159
165
|
browser automation limitations, or local skill files in normal customer-facing
|
|
160
166
|
copy.
|
|
161
167
|
|
|
162
|
-
## Codex Watch
|
|
168
|
+
## Codex Watch Link Handoff
|
|
163
169
|
|
|
164
|
-
When a campaign tool returns `watchUrl`, treat it as
|
|
165
|
-
|
|
166
|
-
`/campaign-builder/{campaignId}?mode=claude` URL
|
|
170
|
+
When a campaign tool returns `watchUrl`, treat it as a user-opened app link, not
|
|
171
|
+
as permission to drive the browser. A valid handoff link must be a direct
|
|
172
|
+
`/campaign-builder/{campaignId}?mode=claude|codex` URL with the auth token and
|
|
173
|
+
workspace routing needed for auto-login. `create_campaign.watchUrl`,
|
|
167
174
|
`create_campaign({ campaignId }).watchUrl`, and `get_campaign.watchUrl` are all
|
|
168
|
-
acceptable only when they return that direct campaign-builder shape
|
|
169
|
-
`workspaceId` used only as a safe routing hint when needed.
|
|
170
|
-
|
|
171
|
-
In Codex Desktop, when in-app browser control is available, open the returned watch link on the user's behalf. After opening it, inspect the browser-visible campaign state, then explain what the user is seeing before continuing with more campaign tools. Use customer language such as:
|
|
172
|
-
|
|
173
|
-
```text
|
|
174
|
-
I’ll open the campaign view and keep it in sync as I build.
|
|
175
|
-
```
|
|
175
|
+
acceptable only when they return that direct campaign-builder shape.
|
|
176
176
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
`get_campaign_navigation_state` orientation checks.
|
|
181
|
-
Use this fallback shape:
|
|
177
|
+
Never call browser-opening tools, shell `open`, Computer Use, or in-app browser
|
|
178
|
+
automation just because a watch link exists. Print the link and tell the user to
|
|
179
|
+
Command-Enter/click it when they want to watch in the app. Use this shape:
|
|
182
180
|
|
|
183
181
|
```text
|
|
184
182
|
Watch link: {watchUrl}
|
|
185
183
|
|
|
186
|
-
|
|
187
|
-
here as I build.
|
|
184
|
+
Command-Enter or click that link to watch the campaign in Sellable. I’ll keep
|
|
185
|
+
the brief, lead source, and message-review steps explicit here as I build.
|
|
188
186
|
```
|
|
189
187
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
`create_campaign({ campaignId })` or `get_campaign
|
|
193
|
-
claim the browser was opened, inspected, or
|
|
194
|
-
|
|
195
|
-
it was opened and observed.
|
|
188
|
+
The watch link should auto-login through the token in the URL. If the user says
|
|
189
|
+
the link lands on auth, 404, permission, blank, or a visible error state, recover
|
|
190
|
+
a fresh watch link once with `create_campaign({ campaignId })` or `get_campaign`
|
|
191
|
+
and print that link. Do not claim the browser was opened, inspected, or
|
|
192
|
+
synchronized.
|
|
196
193
|
|
|
197
194
|
After every `update_campaign({ campaignId, currentStep })`, use
|
|
198
195
|
`get_campaign_navigation_state` when available as a compact orientation check:
|
|
199
|
-
match the
|
|
200
|
-
state in one sentence, and only then continue. Sender selection belongs
|
|
196
|
+
match the saved campaign state to the expected watch-link step, explain the
|
|
197
|
+
current state in one sentence, and only then continue. Sender selection belongs
|
|
198
|
+
at Settings after message approval and 10-row validation. After message
|
|
199
|
+
validation, use Settings to help the user connect or select a LinkedIn sender.
|
|
200
|
+
Explain Slack reply review before launch. After sender selection, attach the
|
|
201
|
+
recommended sequence and move the watched UI to Send. Do not start the campaign
|
|
202
|
+
or trigger a live send unless the user explicitly confirms that launch action
|
|
203
|
+
outside UAT.
|
|
201
204
|
|
|
202
205
|
## Names To Use
|
|
203
206
|
|
|
@@ -186,8 +186,10 @@ Optional debug/UAT draft directory, disabled in normal customer runs:
|
|
|
186
186
|
`references/watch-guide-narration.md` for the compact screen contract. Do not
|
|
187
187
|
send separate guide headline/body args. In Find Leads, the static stage label
|
|
188
188
|
is not enough: name the active lane/provider, what search or sample you are
|
|
189
|
-
trying, and why it fits this campaign.
|
|
190
|
-
|
|
189
|
+
trying, and why it fits this campaign. When the source recommendation and
|
|
190
|
+
counts/sample quality are ready in chat, update the guide to tell the user to
|
|
191
|
+
review and approve the source in Codex/Claude instead of saying `I'll show`
|
|
192
|
+
the recommendation later. Do not promise time estimates in guide copy.
|
|
191
193
|
- Every approval gate must include live campaign access after the readable inline
|
|
192
194
|
content. Show a short `Watch link:` line with the campaign link when a
|
|
193
195
|
campaign shell exists. In normal customer runs, do not show `Open artifact:`
|
|
@@ -580,6 +582,21 @@ workstreams`, `in parallel`, or `background` unless parallel branches were
|
|
|
580
582
|
`attach_recommended_sequence`, `attach_sequence`, and `start_campaign`.
|
|
581
583
|
`create_campaign`, `update_campaign`, and `save_rubrics` are allowed only when
|
|
582
584
|
the active `flow.v2.json` step allows them.
|
|
585
|
+
- When source approval moves into import, keep chat and watch narration in the
|
|
586
|
+
same moment. If chat says import is starting, send `watchNarration` with
|
|
587
|
+
`stage: "review-batch"`, current-tense copy such as `Importing the review
|
|
588
|
+
batch`, and a no-launch safety note. Do not leave the guide saying
|
|
589
|
+
`source approved` or `I'll show the review-batch outcome` once import is
|
|
590
|
+
starting.
|
|
591
|
+
- After Lead Fit Builder saves rubrics, move the watched browser to Filter
|
|
592
|
+
Leads before waiting for message work to finish. Persist
|
|
593
|
+
`enableICPFilters: true`, `currentStep: "apply-icp-rubric"`, and
|
|
594
|
+
`watchNarration.stage: "fit-message"` so the user can see fit filtering
|
|
595
|
+
happen while the first message sample finishes. After message approval,
|
|
596
|
+
persist `useMessagingTemplate: true` and keep `enableICPFilters: true`;
|
|
597
|
+
sample validation then runs the review-batch cascade, and the user should be
|
|
598
|
+
walked through fit results, generated message results, and
|
|
599
|
+
Settings/sender/sequence handoff.
|
|
583
600
|
- During pre-import validation, do not call `check_rubric`; use the lead-filter
|
|
584
601
|
artifacts and only use campaign-backed scoring after Step 13 imports the
|
|
585
602
|
15-lead test batch.
|
|
@@ -788,7 +805,9 @@ Required behavior:
|
|
|
788
805
|
`lead-review.md` focused on source evidence, not host-agent availability.
|
|
789
806
|
- Branch A: LinkedIn Engagement / active LinkedIn posts (internal provider:
|
|
790
807
|
Signals / `signal-discovery`). Search relevant keyword lanes, review
|
|
791
|
-
finalist posts,
|
|
808
|
+
finalist posts, promote the sampled posts with `select_promising_posts`
|
|
809
|
+
before fetching engagers when a campaign shell exists, fetch top-post
|
|
810
|
+
engagers, and estimate warm-fit volume.
|
|
792
811
|
- Branch B: Sales Nav / title + company filters. Run preview filters, inspect
|
|
793
812
|
preview rows, and estimate scalable-fit volume.
|
|
794
813
|
- Branch C: Prospeo Contact / domains only when the campaign has a
|
|
@@ -1002,7 +1021,9 @@ lower confidence instead of presenting a precise estimate.
|
|
|
1002
1021
|
|
|
1003
1022
|
For Signals, default to sampling a few promising posts first rather than trying
|
|
1004
1023
|
to prove the entire source can scale before the user sees evidence. Pick 3-5
|
|
1005
|
-
fresh, high-density posts when available
|
|
1024
|
+
fresh, high-density posts when available. In campaign-attached watch runs,
|
|
1025
|
+
promote those posts with `select_promising_posts` before sampling so the user
|
|
1026
|
+
sees which posts are being tested. Then sample engagers, show fit rate, and
|
|
1006
1027
|
state how many additional posts could be added/scraped if that first sample is
|
|
1007
1028
|
good but volume is low.
|
|
1008
1029
|
|
|
@@ -330,11 +330,12 @@
|
|
|
330
330
|
"campaignBrief"
|
|
331
331
|
],
|
|
332
332
|
"watchUrlSource": "create_campaign.watchUrl",
|
|
333
|
-
"requiredWatchUrlShape": "direct /campaign-builder/{campaignId}?mode={claude|codex} watch URL",
|
|
333
|
+
"requiredWatchUrlShape": "direct /campaign-builder/{campaignId}?mode={claude|codex} watch URL with token auto-login and workspace routing",
|
|
334
334
|
"codexBrowserHandoff": {
|
|
335
|
-
"openWhenAvailable":
|
|
336
|
-
"
|
|
337
|
-
"
|
|
335
|
+
"openWhenAvailable": false,
|
|
336
|
+
"printWatchLinkOnly": true,
|
|
337
|
+
"tellUserCommandEnterOrClick": true,
|
|
338
|
+
"mustNotUseBrowserAutomation": true,
|
|
338
339
|
"fallbackMustNotClaimInspection": true,
|
|
339
340
|
"recoverFreshSignedLinkOnceOnOpenFailure": true
|
|
340
341
|
},
|
|
@@ -540,7 +541,7 @@
|
|
|
540
541
|
"uploadedDomains": "prospeo"
|
|
541
542
|
},
|
|
542
543
|
"when": "before_background_source_scouts",
|
|
543
|
-
"rule": "Choose the likely primary visible source lane from source intake, brief preference, or the best first lane the main thread will actually search. Send watchNarration with stage find-leads that says what search was tried or is being tried next, what sample is being checked, and why it helps this campaign. Do this before waiting on background scouts so watch mode never sits on only Pick Provider while source work happens elsewhere."
|
|
544
|
+
"rule": "Choose the likely primary visible source lane from source intake, brief preference, or the best first lane the main thread will actually search. Send watchNarration with stage find-leads that says what search was tried or is being tried next, what sample is being checked, and why it helps this campaign. For Signal Discovery sampling, promote/select the posts with select_promising_posts before fetch_post_engagers so the watched table shows the exact posts being sampled; the guide copy should say Codex is pulling sample engagers from these posts to confirm the ICP is actually engaging and the source is viable. Do this before waiting on background scouts so watch mode never sits on only Pick Provider while source work happens elsewhere."
|
|
544
545
|
},
|
|
545
546
|
{
|
|
546
547
|
"action": "run_first_campaign_attached_source_search",
|
|
@@ -744,6 +745,7 @@
|
|
|
744
745
|
"artifactLinkTiming": "before_next_step_or_revision_question",
|
|
745
746
|
"doNotCompressToSummaryOnly": false,
|
|
746
747
|
"doNotRenderArtifactLinksOnly": true,
|
|
748
|
+
"sourceRecommendationReadyWatchRule": "When the source recommendation decision card is ready in chat with counts and sample quality, update watchNarration to a find-leads chat-handoff frame. Use a headline like `Review the source in Codex`, body copy that says the browser is showing the evaluated source/results, and nextAction like `Approve in Codex`. Do not keep future-tense copy like `I'll show a source recommendation` after the decision is visible. Include a safety note that no leads import until the user approves the source.",
|
|
747
749
|
"chatRenderRule": "Show a slim rendered-Markdown decision summary only, never a fenced code block. The first sentence must make the decision explicit: 'I recommend {primary source} using {exact filter/source recipe}. The runner-up is {source} because {reason}.' Use indexed sections and short bullets: recommendation, Primary source and filters, Runner-up sources, why it won, Quick numbers with one provider/source angle per bullet, raw volume, sampled fit count as n/N only (no percentages), estimated good-fit range after cleanup, activity/warmth basis, confidence note, 3-5 representative sample leads, and one tradeoff. Do not forecast connection acceptance rates, reply rates, meetings, pipeline, revenue, or ROI unless the user supplied verified benchmark data for this exact workspace/sender. If Signals was searched or considered, include two compact inline Markdown tables before the recommendation is treated as final: Signal keyword lanes with keyword lane, timeframe, posts found, and finalist posts reviewed; and LinkedIn posts sampled with post URL/title, author/topic, age, engagers, sampled engagers, good fits as n/N only, estimated usable prospects per post, and use/discard decision. Default to selecting a few promising Signals posts for the first sample instead of trying to prove full Signals scale up front; if the sample is good but volume is low, say how many more posts to add/scrape next. Do not skip or discard Signals based only on raw post count or vibes; show the post-level math first, or explicitly say no engagers could be fetched and lower confidence. Keep discarded paths, full sample rows, and lead-sample.json details in lead-review.md. Do not show plain filesystem paths unless links cannot be created."
|
|
748
750
|
},
|
|
749
751
|
{
|
|
@@ -760,6 +762,7 @@
|
|
|
760
762
|
"selectedLeadListId stays the source list and workflowTableId is the campaign table"
|
|
761
763
|
],
|
|
762
764
|
"watchNarrationRule": "Do not include time estimates. Say what review-batch work is happening and what the user will approve next.",
|
|
765
|
+
"sourceApprovedImportRule": "After source approval, if chat says import is starting, send review-batch watchNarration with current-tense import copy. Headline should be like `Importing the review batch`; body should say the browser is still showing the approved source leads while only the bounded 15-row review batch is imported. Do not use source-approved or future-tense outcome copy once import is starting.",
|
|
763
766
|
"chatRenderRule": "Lead source is set. Next import and confirm only the first 15-row review batch into the campaign table. After workflowTableId exists, run the fit-filter and message-generation workstreams from the campaign table sample. If real parallel MCP/tool branches or host subagents were actually launched, say so; otherwise say the branches will run sequentially. Never claim parallelism unless parallel execution actually started."
|
|
764
767
|
},
|
|
765
768
|
{
|
|
@@ -790,6 +793,7 @@
|
|
|
790
793
|
"leadSourceProvider",
|
|
791
794
|
"providerSearchAssociation",
|
|
792
795
|
"currentStep: primary provider step (signal-discovery, sales-nav, prospeo/contact-search, saved-lists, or leads)",
|
|
796
|
+
"watchNarration: find-leads source recommendation ready, or review-batch import starting when source approval already happened",
|
|
793
797
|
"selectedLeadListId as source list id only for existing-list or supplied-list preview"
|
|
794
798
|
],
|
|
795
799
|
"fallback": "Stop if campaignId is missing; the source must be attached to the existing CampaignOffer before import.",
|
|
@@ -870,7 +874,8 @@
|
|
|
870
874
|
"onMissingCampaignAttachedSource": "stop_before_import_and_route_to_find_leads; source scouts must attach searches/selections with campaignOfferId before Step 13"
|
|
871
875
|
},
|
|
872
876
|
{
|
|
873
|
-
"action": "watch_mode_orient"
|
|
877
|
+
"action": "watch_mode_orient",
|
|
878
|
+
"watchNarrationRule": "Before import_leads or confirm_lead_list starts, align the guide with chat by setting review-batch watchNarration to current-tense import copy. Use a headline like `Importing the review batch`; explain that the browser may still show the approved source leads while Codex imports only the bounded 15-row review batch; include a no-launch safety note."
|
|
874
879
|
},
|
|
875
880
|
{
|
|
876
881
|
"tool": "import_leads",
|
|
@@ -1039,17 +1044,17 @@
|
|
|
1039
1044
|
"fallback": "If real parallel branches are unavailable or the named agents are absent, run filter-leads and then message-generation in the parent thread with product MCP tools/assets. Do not customer-surface agent install status, and do not claim background or parallel work in that fallback."
|
|
1040
1045
|
},
|
|
1041
1046
|
{
|
|
1042
|
-
"action": "
|
|
1047
|
+
"action": "wait_for_lead_filter_artifact",
|
|
1043
1048
|
"requiredArtifacts": [
|
|
1044
|
-
"lead-filter.md"
|
|
1045
|
-
"message-validation.md"
|
|
1049
|
+
"lead-filter.md"
|
|
1046
1050
|
],
|
|
1047
1051
|
"optionalArtifacts": [
|
|
1048
1052
|
"rubric.json",
|
|
1053
|
+
"message-validation.md",
|
|
1049
1054
|
"message-prep.md",
|
|
1050
1055
|
"message-candidate-drafts.md"
|
|
1051
1056
|
],
|
|
1052
|
-
"
|
|
1057
|
+
"rule": "Do not wait for message-validation.md before saving rubrics and moving the watched browser to Filter Leads. Message work may still be running while the user watches fit filtering."
|
|
1053
1058
|
},
|
|
1054
1059
|
{
|
|
1055
1060
|
"action": "save_filter_rubrics_to_campaign",
|
|
@@ -1069,6 +1074,39 @@
|
|
|
1069
1074
|
],
|
|
1070
1075
|
"writesCampaignState": "leadScoringRubrics",
|
|
1071
1076
|
"requiredBeforeCascade": true
|
|
1077
|
+
},
|
|
1078
|
+
{
|
|
1079
|
+
"action": "advance_watch_to_filter_leads_after_rubrics_saved",
|
|
1080
|
+
"tool": "update_campaign",
|
|
1081
|
+
"requires": [
|
|
1082
|
+
"campaignId",
|
|
1083
|
+
"workflowTableId",
|
|
1084
|
+
"leadScoringRubrics"
|
|
1085
|
+
],
|
|
1086
|
+
"requiredValues": {
|
|
1087
|
+
"currentStep": "apply-icp-rubric",
|
|
1088
|
+
"enableICPFilters": true,
|
|
1089
|
+
"watchNarration.stage": "fit-message"
|
|
1090
|
+
},
|
|
1091
|
+
"watchNarrationRule": "Headline should be like `Filtering the review batch`. Body should say the browser is on Filter Leads, Codex saved the fit rules, and the review rows are being checked while the first message sample finishes. Next should point to message review.",
|
|
1092
|
+
"when": "after_save_rubrics_succeeds_before_waiting_for_message_validation",
|
|
1093
|
+
"writesCampaignState": "currentStep:apply-icp-rubric"
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
"tool": "get_campaign_navigation_state",
|
|
1097
|
+
"purpose": "confirm the watched UI moved to Filter Leads after rubrics saved",
|
|
1098
|
+
"optional": true
|
|
1099
|
+
},
|
|
1100
|
+
{
|
|
1101
|
+
"action": "wait_for_message_validation_artifact",
|
|
1102
|
+
"requiredArtifacts": [
|
|
1103
|
+
"message-validation.md"
|
|
1104
|
+
],
|
|
1105
|
+
"optionalArtifacts": [
|
|
1106
|
+
"message-prep.md",
|
|
1107
|
+
"message-candidate-drafts.md"
|
|
1108
|
+
],
|
|
1109
|
+
"reconciliationRule": "Before entering message-review, verify message-validation.md came from the same brief.md, lead-review.md, lead-sample.json, and saved lead-filter.md. lead-filter.md gates the sample rows; lead-sample.json remains the message sample source."
|
|
1072
1110
|
}
|
|
1073
1111
|
],
|
|
1074
1112
|
"requiredArtifacts": [
|
|
@@ -1090,6 +1128,8 @@
|
|
|
1090
1128
|
"get_subskill_asset",
|
|
1091
1129
|
"get_post_find_leads_scout_registry",
|
|
1092
1130
|
"save_rubrics",
|
|
1131
|
+
"update_campaign",
|
|
1132
|
+
"get_campaign_navigation_state",
|
|
1093
1133
|
"Task",
|
|
1094
1134
|
"spawn_agent",
|
|
1095
1135
|
"AskUserQuestion",
|
|
@@ -1106,7 +1146,6 @@
|
|
|
1106
1146
|
"create_campaign",
|
|
1107
1147
|
"import_leads",
|
|
1108
1148
|
"confirm_lead_list",
|
|
1109
|
-
"update_campaign",
|
|
1110
1149
|
"queue_cells",
|
|
1111
1150
|
"start_campaign",
|
|
1112
1151
|
"check_rubric",
|
|
@@ -1114,7 +1153,7 @@
|
|
|
1114
1153
|
"enrich_with_prospeo",
|
|
1115
1154
|
"bulk_enrich_with_prospeo"
|
|
1116
1155
|
],
|
|
1117
|
-
"watchRequired":
|
|
1156
|
+
"watchRequired": true,
|
|
1118
1157
|
"waitFor": [
|
|
1119
1158
|
"post_lead_workstreams_ready",
|
|
1120
1159
|
"revise_leads",
|
|
@@ -1496,6 +1535,8 @@
|
|
|
1496
1535
|
],
|
|
1497
1536
|
"requiredValues": {
|
|
1498
1537
|
"currentStep": "validate-sample",
|
|
1538
|
+
"enableICPFilters": true,
|
|
1539
|
+
"useMessagingTemplate": true,
|
|
1499
1540
|
"watchNarration.stage": "review-ready"
|
|
1500
1541
|
},
|
|
1501
1542
|
"when": "after_update_campaign_brief_succeeds",
|
|
@@ -1939,7 +1980,7 @@
|
|
|
1939
1980
|
},
|
|
1940
1981
|
{
|
|
1941
1982
|
"tool": "wait_for_campaign_table_ready",
|
|
1942
|
-
"purpose": "
|
|
1983
|
+
"purpose": "wait_for_review_batch_cascade_to_start_returning_filter_results"
|
|
1943
1984
|
},
|
|
1944
1985
|
{
|
|
1945
1986
|
"tool": "get_rows_minimal",
|
|
@@ -1954,11 +1995,12 @@
|
|
|
1954
1995
|
"minPassedCount"
|
|
1955
1996
|
],
|
|
1956
1997
|
"targetCountSource": "stats.totalRows_or_imported_batch_count",
|
|
1957
|
-
"minPassedCountSource": "
|
|
1998
|
+
"minPassedCountSource": "firstPassingRowForMessageStart (1)",
|
|
1958
1999
|
"requiredValues": {
|
|
1959
|
-
"includeRows": false
|
|
2000
|
+
"includeRows": false,
|
|
2001
|
+
"minPassedCount": 1
|
|
1960
2002
|
},
|
|
1961
|
-
"note": "The shell-first flow tests 15 leads first; always pass cohortSize explicitly instead of relying on default 25 behavior. Pass minPassedCount so
|
|
2003
|
+
"note": "The shell-first flow tests 15 leads first; always pass cohortSize explicitly instead of relying on default 25 behavior. Pass minPassedCount=1 so the first passing filtered row unblocks Generate Message observation instead of waiting for every sample row to finish.",
|
|
1962
2004
|
"readVia": "stats_only_tool_result",
|
|
1963
2005
|
"extractFields": [
|
|
1964
2006
|
"ready",
|
|
@@ -1975,7 +2017,7 @@
|
|
|
1975
2017
|
{
|
|
1976
2018
|
"action": "handle_partial_or_timeout_sample",
|
|
1977
2019
|
"when": "wait_for_rubric_results.ready_false_or_reason_timeout",
|
|
1978
|
-
"rule": "Do not repeat waits indefinitely. If active processing is visible, wait once more at most. Otherwise treat returned passRate/stats as a partial sample and stop before Settings with sample_revision_required
|
|
2020
|
+
"rule": "Do not repeat waits indefinitely. If at least one row has passed, move to Step 15 to observe or queue Generate Message for passing rows even when other sample rows are still processing. If zero rows have passed and active processing is visible, wait once more at most. Otherwise treat returned passRate/stats as a partial sample and stop before Settings with sample_revision_required.",
|
|
1979
2021
|
"customerStatus": "sample-needs-revision",
|
|
1980
2022
|
"showFields": [
|
|
1981
2023
|
"passRate.completed",
|
|
@@ -2024,6 +2066,7 @@
|
|
|
2024
2066
|
"revision_round_persists_across_resume",
|
|
2025
2067
|
"wait_for_rubric_results_never_retain_rows_payload_in_tail_context",
|
|
2026
2068
|
"wait_for_rubric_results_targetCount_always_explicit",
|
|
2069
|
+
"first_passing_row_unblocks_generate_message_observation",
|
|
2027
2070
|
"timeout_never_repeats_without_customer_handoff",
|
|
2028
2071
|
"timeout_or_underfloor_sample_never_advances_to_settings",
|
|
2029
2072
|
"review_batch_cascade_waits_for_saved_rubrics_and_approved_template"
|
|
@@ -9,7 +9,9 @@ on every revision round.
|
|
|
9
9
|
We spend a bounded review batch (default 15 rows) to prove fit before the
|
|
10
10
|
user spends credits on hundreds more leads. The sample loop has one job:
|
|
11
11
|
answer the question "do we have enough real passing examples for the user to
|
|
12
|
-
judge this campaign?"
|
|
12
|
+
judge this campaign?" Message generation starts earlier: the first row that
|
|
13
|
+
passes filters is enough to begin observing or queueing Generate Message for
|
|
14
|
+
that passing row.
|
|
13
15
|
|
|
14
16
|
If the answer is yes, proceed to Step 15 messaging for the review batch. If
|
|
15
17
|
the answer is no, diagnose whether the brief is wrong or the list is wrong,
|
|
@@ -48,13 +50,16 @@ auto-revise leads.
|
|
|
48
50
|
|
|
49
51
|
5. check_rubric(sample)
|
|
50
52
|
|
|
51
|
-
6. wait_for_rubric_results(sample, targetCount = <cohortSize
|
|
53
|
+
6. wait_for_rubric_results(sample, targetCount = <cohortSize>, minPassedCount = 1)
|
|
52
54
|
- cohortSize = stats.totalRows of the enrichment batch, or the
|
|
53
55
|
imported batch count
|
|
54
56
|
- default targetCount=15 matches the default review batch, but pass the
|
|
55
57
|
explicit batch count anyway so future larger expansion batches do not
|
|
56
58
|
accidentally stop early
|
|
57
59
|
(see §Known Tool Behaviors #3)
|
|
60
|
+
- minPassedCount=1 means one passing filtered row unblocks Step 15
|
|
61
|
+
Generate Message observation. Do not wait for all sample rows to finish
|
|
62
|
+
before messages start.
|
|
58
63
|
|
|
59
64
|
7. call `wait_for_rubric_results` with `includeRows=false`; extract ONLY:
|
|
60
65
|
- ready: boolean
|
|
@@ -71,11 +76,13 @@ auto-revise leads.
|
|
|
71
76
|
8. if `ready=false` and `reason="timeout"`:
|
|
72
77
|
- do NOT keep polling indefinitely
|
|
73
78
|
- treat the returned passRate/stats as a partial sample
|
|
79
|
+
- if at least one row has passed filters, advance to Step 15 to observe or
|
|
80
|
+
queue Generate Message for currently passing rows
|
|
74
81
|
- if active processing is visible (`processingCount > 0` or a similar
|
|
75
82
|
running-cell stat), one additional wait is allowed
|
|
76
83
|
- otherwise diagnose the partial sample now
|
|
77
|
-
- if
|
|
78
|
-
|
|
84
|
+
- if zero rows have passed and there is no active processing, stop before
|
|
85
|
+
Settings with:
|
|
79
86
|
`Status: sample-needs-revision`
|
|
80
87
|
- show the concrete numbers: completed, passed, pending, pass percent, and
|
|
81
88
|
message count when available
|
|
@@ -89,8 +96,9 @@ auto-revise leads.
|
|
|
89
96
|
projectedPass = round(passInSample / sampleSize * importLimit)
|
|
90
97
|
|
|
91
98
|
10. branch:
|
|
92
|
-
if
|
|
93
|
-
proceed to Step 15 (auto-execute-messaging) with
|
|
99
|
+
if passInSample >= 1:
|
|
100
|
+
proceed to Step 15 (auto-execute-messaging) with currently passing rows
|
|
101
|
+
so Generate Message can start without waiting for the full sample
|
|
94
102
|
else:
|
|
95
103
|
diagnose (see Brief-vs-List Diagnosis below)
|
|
96
104
|
revisionRound += 1
|
|
@@ -166,11 +174,12 @@ and partial stats such as 13/15 scored, 2 passing, 2 messages generated. That is
|
|
|
166
174
|
enough to diagnose an underperforming sample. Waiting again without active
|
|
167
175
|
processing makes the experience feel frozen.
|
|
168
176
|
|
|
169
|
-
Workaround: treat timeout stats as a partial sample. If
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
177
|
+
Workaround: treat timeout stats as a partial sample. If at least one row has
|
|
178
|
+
passed, move to Step 15 and observe or queue Generate Message for the passing
|
|
179
|
+
rows. If zero rows have passed and no active processing is visible, stop at
|
|
180
|
+
`Status: sample-needs-revision` before Settings. Show the completed / passed /
|
|
181
|
+
pending counts and ask whether to revise source, revise filter/rubric, or wait
|
|
182
|
+
once only if active processing is still visible.
|
|
174
183
|
|
|
175
184
|
## Projected Pass Math
|
|
176
185
|
|
|
@@ -67,9 +67,9 @@ Signal Discovery:
|
|
|
67
67
|
```json
|
|
68
68
|
{
|
|
69
69
|
"stage": "find-leads",
|
|
70
|
-
"headline": "Testing
|
|
71
|
-
"visibleState": "
|
|
72
|
-
"agentIntent": "Codex is
|
|
70
|
+
"headline": "Testing warm LinkedIn activity",
|
|
71
|
+
"visibleState": "The promoted posts are the ones being sampled now.",
|
|
72
|
+
"agentIntent": "Codex is pulling sample engagers from these posts to confirm the ICP is actually engaging and that this source is viable.",
|
|
73
73
|
"nextAction": "Review batch"
|
|
74
74
|
}
|
|
75
75
|
```
|
|
@@ -86,6 +86,24 @@ Search iteration:
|
|
|
86
86
|
}
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
Source recommendation ready:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"stage": "find-leads",
|
|
94
|
+
"headline": "Review the source in Codex",
|
|
95
|
+
"visibleState": "The browser is showing the evaluated Signal Discovery source with counts and sample quality.",
|
|
96
|
+
"agentIntent": "Approve LinkedIn Engagement or ask for a source change in chat before any leads import.",
|
|
97
|
+
"nextAction": "Approve in Codex",
|
|
98
|
+
"safety": "No leads import until you approve the source."
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
When the source recommendation, counts, and sample-quality decision are ready in
|
|
103
|
+
chat, update the guide to route the user back to Codex/Claude for approval. Do
|
|
104
|
+
not keep future-tense copy such as `I'll show a source recommendation` once the
|
|
105
|
+
decision card is visible.
|
|
106
|
+
|
|
89
107
|
Review batch:
|
|
90
108
|
|
|
91
109
|
```json
|
|
@@ -98,6 +116,24 @@ Review batch:
|
|
|
98
116
|
}
|
|
99
117
|
```
|
|
100
118
|
|
|
119
|
+
Source approved and import starting:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"stage": "review-batch",
|
|
124
|
+
"headline": "Importing the review batch",
|
|
125
|
+
"visibleState": "The browser is still showing the approved LinkedIn Engagement source leads.",
|
|
126
|
+
"agentIntent": "Codex is importing only the bounded 15-row review batch into the campaign now.",
|
|
127
|
+
"nextAction": "Review batch ready",
|
|
128
|
+
"safety": "This is still a review step; nothing launches or sends."
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
When chat says the source is approved and import is starting, the guide must use
|
|
133
|
+
current-tense import copy. Do not leave the guide in source-approved or
|
|
134
|
+
future-tense copy such as `I'll show the review-batch outcome`; that makes the
|
|
135
|
+
browser guide describe a different moment than chat.
|
|
136
|
+
|
|
101
137
|
Fit + message:
|
|
102
138
|
|
|
103
139
|
```json
|
|
@@ -11,12 +11,13 @@ gating, and handoff read campaign state first.
|
|
|
11
11
|
|
|
12
12
|
## Plumbing Reuse
|
|
13
13
|
|
|
14
|
-
`create_campaign` already returns a
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
`create_campaign` already returns a direct campaign-builder `watchUrl` on the
|
|
15
|
+
response (`CampaignDetail.watchUrl` in `mcp/sellable/src/tools/campaigns.ts`).
|
|
16
|
+
The URL includes the auth token and workspace routing needed for browser
|
|
17
|
+
auto-login, then the app exchanges the token through `/auth/continue` and
|
|
18
|
+
cleans it from the browser URL. V2 does NOT mint a new token, does NOT call a
|
|
19
|
+
different route, and does NOT construct the URL locally. Capture whatever
|
|
20
|
+
`watchUrl` the existing tool returns and surface it verbatim.
|
|
20
21
|
|
|
21
22
|
## Shell-First Link
|
|
22
23
|
|
|
@@ -39,6 +40,8 @@ No leads import and nothing sends yet.
|
|
|
39
40
|
|
|
40
41
|
Watch link: {watchUrl}
|
|
41
42
|
|
|
43
|
+
Command-Enter or click that link to watch it in Sellable.
|
|
44
|
+
|
|
42
45
|
Cool, let's find leads.
|
|
43
46
|
```
|
|
44
47
|
|
|
@@ -93,7 +96,9 @@ cached or reconstructed URL.
|
|
|
93
96
|
## Hard Rules
|
|
94
97
|
|
|
95
98
|
- Never fabricate or reconstruct the URL locally.
|
|
96
|
-
- The watch link must be a
|
|
99
|
+
- The watch link must be a direct campaign-builder URL with token auto-login.
|
|
100
|
+
- Do not call browser-opening tools, shell `open`, Computer Use, or in-app
|
|
101
|
+
browser automation; the user opens the link when they want to watch.
|
|
97
102
|
- Missing `watchUrl` is an error, not a silent skip.
|
|
98
103
|
- The first watch link must be shown only after the v1 brief is on the campaign.
|
|
99
104
|
- A watch link is not approval to import, attach sequence, or start.
|
|
@@ -20,8 +20,8 @@ Every tail run MUST call these tools in this exact order. The tail is
|
|
|
20
20
|
**review-batch cascade-driven**: you kick off Enrich Prospect only for
|
|
21
21
|
the imported review batch, and the workflow engine chains DNC Check →
|
|
22
22
|
ICP Score → Passes Rubric → Generate Message automatically. Your job is
|
|
23
|
-
to START the bounded cascade, WAIT
|
|
24
|
-
and stop for review.
|
|
23
|
+
to START the bounded cascade, WAIT until filter results land, OBSERVE message
|
|
24
|
+
generation as soon as one row passes, and stop for review.
|
|
25
25
|
Do NOT manually run rubric-check, enrich, or message-generation
|
|
26
26
|
tools — the cascade already does them.
|
|
27
27
|
|
|
@@ -43,14 +43,16 @@ Post-import main thread
|
|
|
43
43
|
|
|
44
44
|
Step 14 — kick bounded cascade + observe sample
|
|
45
45
|
queue_cells(cellIds=<review-batch Enrich Prospect cells only>) <-- starts bounded chain
|
|
46
|
-
wait_for_campaign_table_ready # review-batch cascade
|
|
46
|
+
wait_for_campaign_table_ready # wait until review-batch cascade starts returning filter results
|
|
47
47
|
get_rows_minimal # read passesRubric + message cell status per row
|
|
48
|
-
|
|
49
|
-
if
|
|
50
|
-
|
|
48
|
+
wait_for_rubric_results(minPassedCount=1, includeRows=false)
|
|
49
|
+
if at least one row passes: update_campaign(currentStep=auto-execute-messaging)
|
|
50
|
+
compute projectedPass for later reporting / revision decisions
|
|
51
|
+
if zero rows pass: diagnose brief-vs-list; if brief: update_campaign_brief + re-queue + wait
|
|
51
52
|
(check_rubric / bulk_enrich_with_prospeo are NOT called here —
|
|
52
53
|
cascade already did them. wait_for_rubric_results is OK as a
|
|
53
|
-
read-only observation helper if you need to block
|
|
54
|
+
read-only observation helper if you need to block until the first
|
|
55
|
+
passing filtered row exists.)
|
|
54
56
|
|
|
55
57
|
Step 15 — observe messaging
|
|
56
58
|
get_rows_minimal # confirm passing rows have completed Generate Message cells
|
|
@@ -98,9 +100,12 @@ Message` column's http_request writes those cells via the cascade.
|
|
|
98
100
|
- `wait_for_rubric_results` is read-only and OK to use as an
|
|
99
101
|
observation helper when you need to block on rubric completion —
|
|
100
102
|
it does not mutate cells. Prefer `get_rows_minimal` +
|
|
101
|
-
`wait_for_campaign_table_ready` for the primary cascade-
|
|
103
|
+
`wait_for_campaign_table_ready` for the primary cascade-observation
|
|
102
104
|
path, but use `wait_for_rubric_results({ targetCount: cohortSize })`
|
|
103
|
-
when the table-level wait returns before rubric cells finish.
|
|
105
|
+
when the table-level wait returns before rubric cells finish. In
|
|
106
|
+
create-campaign-v2 Step 14, pass `minPassedCount: 1`; one passing
|
|
107
|
+
filtered row is enough to start observing Generate Message, even if
|
|
108
|
+
other sample rows are still processing.
|
|
104
109
|
- `start_campaign` is FORBIDDEN in the autonomous tail. It belongs only
|
|
105
110
|
in the Claude-greenlight path, AFTER the user signals "start". See
|
|
106
111
|
`references/final-handoff-contract.md`.
|
|
@@ -275,9 +280,9 @@ decision tree lives in `references/sample-validation-loop.md`.
|
|
|
275
280
|
|
|
276
281
|
**Step 14 starts the bounded cascade, then observes it.** Step 13 imported the
|
|
277
282
|
review batch only. After `save_rubrics` and the approved message template are
|
|
278
|
-
persisted, Step 14 queues the review-batch Enrich Prospect cells, waits
|
|
279
|
-
|
|
280
|
-
|
|
283
|
+
persisted, Step 14 queues the review-batch Enrich Prospect cells, waits until
|
|
284
|
+
filter results start landing, then moves to message observation as soon as one
|
|
285
|
+
row passes. It does NOT call `check_rubric`, `bulk_enrich_with_prospeo`, or any
|
|
281
286
|
other direct enrichment/scoring tool.
|
|
282
287
|
|
|
283
288
|
Shape:
|
|
@@ -286,23 +291,23 @@ Shape:
|
|
|
286
291
|
queue_cells({ tableId: workflowTableId, cellIds: reviewBatchEnrichCellIds })
|
|
287
292
|
wait_for_campaign_table_ready({ tableId: workflowTableId })
|
|
288
293
|
get_rows_minimal({ tableId: workflowTableId })
|
|
289
|
-
wait_for_rubric_results({ tableId: workflowTableId, targetCount: cohortSize, minPassedCount:
|
|
294
|
+
wait_for_rubric_results({ tableId: workflowTableId, targetCount: cohortSize, minPassedCount: 1, includeRows: false })
|
|
290
295
|
passInSample = count of first sampleSize review-batch rows with passesRubric === true
|
|
291
296
|
projectedPass = round(passInSample / sampleSize * importLimit)
|
|
292
297
|
|
|
293
|
-
if wait_for_rubric_results.ready === true and
|
|
294
|
-
|
|
295
|
-
|
|
298
|
+
if wait_for_rubric_results.ready === true and passRate.passed >= 1:
|
|
299
|
+
advance to Step 15 to observe or queue Generate Message for currently passing rows
|
|
300
|
+
do not wait for every sample row to finish before message generation starts
|
|
296
301
|
else if wait_for_rubric_results.ready === false and reason === "timeout":
|
|
297
302
|
use the partial passRate/stats as the sample diagnostic
|
|
298
|
-
if
|
|
303
|
+
if passRate.passed >= 1:
|
|
304
|
+
advance to Step 15 to observe/queue Generate Message for passing rows
|
|
305
|
+
else if active processing is visible:
|
|
299
306
|
wait one more time at most
|
|
300
307
|
else:
|
|
301
308
|
stop before Settings with Status: sample-needs-revision
|
|
302
309
|
show completed, passed, pending, pass percent, and message count when available
|
|
303
310
|
ask whether to revise source, revise filter/rubric, or wait once only if still processing
|
|
304
|
-
else if projectedPass >= minProjectedPass:
|
|
305
|
-
advance to Step 15
|
|
306
311
|
else:
|
|
307
312
|
diagnose brief-vs-list per sample-validation-loop.md
|
|
308
313
|
- brief: autonomous update_campaign_brief, then re-kick cascade
|
|
@@ -94,6 +94,8 @@ Use conservative logic:
|
|
|
94
94
|
1. Start from recent strong-post inventory and sample quality, not raw total post count.
|
|
95
95
|
2. If the lane looks promising, sample real engagers before claiming a reachable-lead estimate:
|
|
96
96
|
- choose 1-2 promising posts
|
|
97
|
+
- in campaign-attached runs, call `select_promising_posts` first to promote
|
|
98
|
+
those posts in the watched Signal Discovery UI before sampling engagers
|
|
97
99
|
- call `fetch_post_engagers`
|
|
98
100
|
- fetch only a representative first sample, not a full scrape
|
|
99
101
|
- default to `limit: 25` for one post or `limit: 20` per post when checking two posts
|
|
@@ -325,6 +327,11 @@ Select the best posts to scrape AND provide headline ICP criteria.
|
|
|
325
327
|
Use `selectionTarget` and `recommendedPostIds` returned by `search_signals`
|
|
326
328
|
(default selectionTarget is 3).
|
|
327
329
|
|
|
330
|
+
For campaign-attached viability sampling, this is also the promote step: call it
|
|
331
|
+
before `fetch_post_engagers` so the user sees the exact posts being sampled in
|
|
332
|
+
the watched Signal Discovery table. Use `selectionMode: "replace"` for the first
|
|
333
|
+
sample set unless the user explicitly wants to add to existing promoted posts.
|
|
334
|
+
|
|
328
335
|
```json
|
|
329
336
|
select_promising_posts({
|
|
330
337
|
"campaignOfferId": "cmp_xxx",
|