@sellable/mcp 0.1.111 → 0.1.113
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/dist/tools/campaigns.d.ts +77 -0
- package/dist/tools/campaigns.js +63 -4
- package/dist/tools/navigation.js +2 -2
- package/dist/tools/rubrics.js +7 -5
- package/package.json +1 -1
- package/skills/create-campaign-v2/SKILL.md +33 -11
- package/skills/create-campaign-v2/core/flow.v2.json +88 -37
- package/skills/create-campaign-v2/references/sample-validation-loop.md +20 -11
- package/skills/create-campaign-v2/references/watch-guide-narration.md +177 -0
- package/skills/create-campaign-v2/references/watch-link-handoff.md +1 -1
- package/skills/create-campaign-v2-tail/SKILL.md +24 -19
|
@@ -9,6 +9,7 @@ declare const LEAD_SOURCE_PROVIDERS: {
|
|
|
9
9
|
};
|
|
10
10
|
type LeadSourceProvider = (typeof LEAD_SOURCE_PROVIDERS)[keyof typeof LEAD_SOURCE_PROVIDERS];
|
|
11
11
|
export declare function buildWatchUrl(config: Pick<ReturnType<typeof getConfig>, "apiUrl" | "token" | "activeWorkspaceId" | "workspaceId">, path: string): string;
|
|
12
|
+
export declare function getCampaignBuilderWatchModeParam(): "claude" | "codex";
|
|
12
13
|
export interface Campaign {
|
|
13
14
|
id: string;
|
|
14
15
|
name: string;
|
|
@@ -124,6 +125,7 @@ export interface CreateCampaignInput {
|
|
|
124
125
|
campaignBrief?: string;
|
|
125
126
|
messageGenerationMode?: "template" | "ai-generated";
|
|
126
127
|
currentStep?: string | null;
|
|
128
|
+
watchNarration?: unknown;
|
|
127
129
|
leadSourceType?: string | null;
|
|
128
130
|
leadSourceProvider?: LeadSourceProvider | "apollo" | null;
|
|
129
131
|
selectedLeadListId?: string | null;
|
|
@@ -137,6 +139,7 @@ export interface UpdateCampaignInput {
|
|
|
137
139
|
leadSourceProvider?: LeadSourceProvider | "apollo" | null;
|
|
138
140
|
selectedLeadListId?: string | null;
|
|
139
141
|
currentStep?: string | null;
|
|
142
|
+
watchNarration?: unknown;
|
|
140
143
|
interactionMode?: InteractionMode;
|
|
141
144
|
enableICPFilters?: boolean | null;
|
|
142
145
|
useMessagingTemplate?: boolean | null;
|
|
@@ -165,6 +168,7 @@ export declare const campaignToolDefinitions: ({
|
|
|
165
168
|
campaignBrief?: undefined;
|
|
166
169
|
messageGenerationMode?: undefined;
|
|
167
170
|
currentStep?: undefined;
|
|
171
|
+
watchNarration?: undefined;
|
|
168
172
|
leadSourceType?: undefined;
|
|
169
173
|
leadSourceProvider?: undefined;
|
|
170
174
|
selectedLeadListId?: undefined;
|
|
@@ -200,6 +204,7 @@ export declare const campaignToolDefinitions: ({
|
|
|
200
204
|
campaignBrief?: undefined;
|
|
201
205
|
messageGenerationMode?: undefined;
|
|
202
206
|
currentStep?: undefined;
|
|
207
|
+
watchNarration?: undefined;
|
|
203
208
|
leadSourceType?: undefined;
|
|
204
209
|
leadSourceProvider?: undefined;
|
|
205
210
|
selectedLeadListId?: undefined;
|
|
@@ -278,6 +283,7 @@ export declare const campaignToolDefinitions: ({
|
|
|
278
283
|
campaignBrief?: undefined;
|
|
279
284
|
messageGenerationMode?: undefined;
|
|
280
285
|
currentStep?: undefined;
|
|
286
|
+
watchNarration?: undefined;
|
|
281
287
|
leadSourceType?: undefined;
|
|
282
288
|
leadSourceProvider?: undefined;
|
|
283
289
|
selectedLeadListId?: undefined;
|
|
@@ -330,6 +336,41 @@ export declare const campaignToolDefinitions: ({
|
|
|
330
336
|
type: string[];
|
|
331
337
|
description: string;
|
|
332
338
|
};
|
|
339
|
+
watchNarration: {
|
|
340
|
+
type: string;
|
|
341
|
+
description: string;
|
|
342
|
+
properties: {
|
|
343
|
+
stage: {
|
|
344
|
+
type: string;
|
|
345
|
+
enum: string[];
|
|
346
|
+
};
|
|
347
|
+
headline: {
|
|
348
|
+
type: string;
|
|
349
|
+
};
|
|
350
|
+
visibleState: {
|
|
351
|
+
type: string;
|
|
352
|
+
};
|
|
353
|
+
agentIntent: {
|
|
354
|
+
type: string;
|
|
355
|
+
};
|
|
356
|
+
nextAction: {
|
|
357
|
+
type: string[];
|
|
358
|
+
};
|
|
359
|
+
safety: {
|
|
360
|
+
type: string[];
|
|
361
|
+
};
|
|
362
|
+
progressLabel: {
|
|
363
|
+
type: string[];
|
|
364
|
+
};
|
|
365
|
+
blockedReason: {
|
|
366
|
+
type: string[];
|
|
367
|
+
};
|
|
368
|
+
workerStatuses: {
|
|
369
|
+
type: string;
|
|
370
|
+
};
|
|
371
|
+
};
|
|
372
|
+
required: string[];
|
|
373
|
+
};
|
|
333
374
|
leadSourceType: {
|
|
334
375
|
type: string[];
|
|
335
376
|
description: string;
|
|
@@ -399,6 +440,41 @@ export declare const campaignToolDefinitions: ({
|
|
|
399
440
|
type: string[];
|
|
400
441
|
description: string;
|
|
401
442
|
};
|
|
443
|
+
watchNarration: {
|
|
444
|
+
type: string;
|
|
445
|
+
description: string;
|
|
446
|
+
properties: {
|
|
447
|
+
stage: {
|
|
448
|
+
type: string;
|
|
449
|
+
enum: string[];
|
|
450
|
+
};
|
|
451
|
+
headline: {
|
|
452
|
+
type: string;
|
|
453
|
+
};
|
|
454
|
+
visibleState: {
|
|
455
|
+
type: string;
|
|
456
|
+
};
|
|
457
|
+
agentIntent: {
|
|
458
|
+
type: string;
|
|
459
|
+
};
|
|
460
|
+
nextAction: {
|
|
461
|
+
type: string[];
|
|
462
|
+
};
|
|
463
|
+
safety: {
|
|
464
|
+
type: string[];
|
|
465
|
+
};
|
|
466
|
+
progressLabel: {
|
|
467
|
+
type: string[];
|
|
468
|
+
};
|
|
469
|
+
blockedReason: {
|
|
470
|
+
type: string[];
|
|
471
|
+
};
|
|
472
|
+
workerStatuses: {
|
|
473
|
+
type: string;
|
|
474
|
+
};
|
|
475
|
+
};
|
|
476
|
+
required: string[];
|
|
477
|
+
};
|
|
402
478
|
interactionMode: {
|
|
403
479
|
type: string;
|
|
404
480
|
enum: string[];
|
|
@@ -466,6 +542,7 @@ export declare const campaignToolDefinitions: ({
|
|
|
466
542
|
offerPositioning?: undefined;
|
|
467
543
|
messageGenerationMode?: undefined;
|
|
468
544
|
currentStep?: undefined;
|
|
545
|
+
watchNarration?: undefined;
|
|
469
546
|
leadSourceType?: undefined;
|
|
470
547
|
leadSourceProvider?: undefined;
|
|
471
548
|
selectedLeadListId?: undefined;
|
package/dist/tools/campaigns.js
CHANGED
|
@@ -27,6 +27,15 @@ export function buildWatchUrl(config, path) {
|
|
|
27
27
|
}
|
|
28
28
|
return url.toString();
|
|
29
29
|
}
|
|
30
|
+
export function getCampaignBuilderWatchModeParam() {
|
|
31
|
+
const explicit = process.env.SELLABLE_WATCH_MODE_DRIVER?.trim().toLowerCase();
|
|
32
|
+
if (explicit === "claude" || explicit === "codex")
|
|
33
|
+
return explicit;
|
|
34
|
+
return process.env.CODEX_HOME ? "codex" : "claude";
|
|
35
|
+
}
|
|
36
|
+
function buildCampaignBuilderWatchPath(campaignId) {
|
|
37
|
+
return `/campaign-builder/${campaignId}?mode=${getCampaignBuilderWatchModeParam()}`;
|
|
38
|
+
}
|
|
30
39
|
function isLinkedInProfileUrl(input) {
|
|
31
40
|
try {
|
|
32
41
|
const url = new URL(input);
|
|
@@ -176,6 +185,31 @@ export const campaignToolDefinitions = [
|
|
|
176
185
|
type: ["string", "null"],
|
|
177
186
|
description: "Workflow step ID (headless or UI step ID such as filter-rules)",
|
|
178
187
|
},
|
|
188
|
+
watchNarration: {
|
|
189
|
+
type: "object",
|
|
190
|
+
description: "Single structured watch-guide narration object to send with currentStep changes. Include stage, headline, visibleState, agentIntent, and nextAction so the browser guide mirrors Codex progress.",
|
|
191
|
+
properties: {
|
|
192
|
+
stage: {
|
|
193
|
+
type: "string",
|
|
194
|
+
enum: [
|
|
195
|
+
"brief-review",
|
|
196
|
+
"find-leads",
|
|
197
|
+
"review-batch",
|
|
198
|
+
"fit-message",
|
|
199
|
+
"review-ready",
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
headline: { type: "string" },
|
|
203
|
+
visibleState: { type: "string" },
|
|
204
|
+
agentIntent: { type: "string" },
|
|
205
|
+
nextAction: { type: ["string", "null"] },
|
|
206
|
+
safety: { type: ["string", "null"] },
|
|
207
|
+
progressLabel: { type: ["string", "null"] },
|
|
208
|
+
blockedReason: { type: ["string", "null"] },
|
|
209
|
+
workerStatuses: { type: "object" },
|
|
210
|
+
},
|
|
211
|
+
required: ["stage", "headline", "visibleState", "agentIntent"],
|
|
212
|
+
},
|
|
179
213
|
leadSourceType: {
|
|
180
214
|
type: ["string", "null"],
|
|
181
215
|
description: "Lead source type (existing | new)",
|
|
@@ -244,10 +278,35 @@ export const campaignToolDefinitions = [
|
|
|
244
278
|
type: ["string", "null"],
|
|
245
279
|
description: "Headless workflow step ID",
|
|
246
280
|
},
|
|
281
|
+
watchNarration: {
|
|
282
|
+
type: "object",
|
|
283
|
+
description: "Single structured watch-guide narration object to send with currentStep changes. Include stage, headline, visibleState, agentIntent, and nextAction so the browser guide mirrors Codex progress. Do not send separate guideHeadline/guideBody fields.",
|
|
284
|
+
properties: {
|
|
285
|
+
stage: {
|
|
286
|
+
type: "string",
|
|
287
|
+
enum: [
|
|
288
|
+
"brief-review",
|
|
289
|
+
"find-leads",
|
|
290
|
+
"review-batch",
|
|
291
|
+
"fit-message",
|
|
292
|
+
"review-ready",
|
|
293
|
+
],
|
|
294
|
+
},
|
|
295
|
+
headline: { type: "string" },
|
|
296
|
+
visibleState: { type: "string" },
|
|
297
|
+
agentIntent: { type: "string" },
|
|
298
|
+
nextAction: { type: ["string", "null"] },
|
|
299
|
+
safety: { type: ["string", "null"] },
|
|
300
|
+
progressLabel: { type: ["string", "null"] },
|
|
301
|
+
blockedReason: { type: ["string", "null"] },
|
|
302
|
+
workerStatuses: { type: "object" },
|
|
303
|
+
},
|
|
304
|
+
required: ["stage", "headline", "visibleState", "agentIntent"],
|
|
305
|
+
},
|
|
247
306
|
interactionMode: {
|
|
248
307
|
type: "string",
|
|
249
308
|
enum: ["step-by-step", "autonomous", "ask-when-needed"],
|
|
250
|
-
description: "Execution style for
|
|
309
|
+
description: "Execution style for Find Leads steps (step-by-step | autonomous | ask-when-needed).",
|
|
251
310
|
},
|
|
252
311
|
enableICPFilters: {
|
|
253
312
|
type: ["boolean", "null"],
|
|
@@ -335,7 +394,7 @@ export async function getCampaign(campaignId) {
|
|
|
335
394
|
api.get(`/api/v3/mcp/campaigns/${campaignId}`),
|
|
336
395
|
fetchCampaignRubrics(campaignId).catch(() => null),
|
|
337
396
|
]);
|
|
338
|
-
const watchUrl = buildWatchUrl(config,
|
|
397
|
+
const watchUrl = buildWatchUrl(config, buildCampaignBuilderWatchPath(campaignId));
|
|
339
398
|
// Merge rubrics into campaignOffer
|
|
340
399
|
const rubrics = (rubricsResult?.rubrics || []).map((r) => ({
|
|
341
400
|
id: r.id || "",
|
|
@@ -590,7 +649,7 @@ export async function createCampaign(input) {
|
|
|
590
649
|
// Idempotent resume path
|
|
591
650
|
if (input.campaignId) {
|
|
592
651
|
const existing = await api.get(`/api/v2/campaign-offers/${input.campaignId}`);
|
|
593
|
-
const watchUrl = buildWatchUrl(config,
|
|
652
|
+
const watchUrl = buildWatchUrl(config, buildCampaignBuilderWatchPath(existing.id));
|
|
594
653
|
const hasCreateFields = input.name ||
|
|
595
654
|
input.clientProspectId ||
|
|
596
655
|
input.offerPositioning !== undefined ||
|
|
@@ -708,7 +767,7 @@ export async function createCampaign(input) {
|
|
|
708
767
|
},
|
|
709
768
|
};
|
|
710
769
|
const result = await api.post(`/api/v2/campaign-offers`, formattedInput);
|
|
711
|
-
const watchUrl = buildWatchUrl(config,
|
|
770
|
+
const watchUrl = buildWatchUrl(config, buildCampaignBuilderWatchPath(result.id));
|
|
712
771
|
// Serialize to only essential fields for context efficiency
|
|
713
772
|
return {
|
|
714
773
|
id: result.id,
|
package/dist/tools/navigation.js
CHANGED
|
@@ -415,8 +415,8 @@ function parseWatchUrl(watchUrl, campaignId) {
|
|
|
415
415
|
direct: false,
|
|
416
416
|
redirectPath: decodedRedirect,
|
|
417
417
|
warning: isLegacySigned
|
|
418
|
-
? "Watch URL still uses a tokenized /auth/continue handoff. Return the safe direct /campaign-builder/{campaignId}?mode=claude watch URL instead."
|
|
419
|
-
: "Watch URL is not a safe direct /campaign-builder/{campaignId}?mode=claude watch link.",
|
|
418
|
+
? "Watch URL still uses a tokenized /auth/continue handoff. Return the safe direct /campaign-builder/{campaignId}?mode={claude|codex} watch URL instead."
|
|
419
|
+
: "Watch URL is not a safe direct /campaign-builder/{campaignId}?mode={claude|codex} watch link.",
|
|
420
420
|
};
|
|
421
421
|
}
|
|
422
422
|
catch {
|
package/dist/tools/rubrics.js
CHANGED
|
@@ -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,
|
package/package.json
CHANGED
|
@@ -165,7 +165,7 @@ Optional debug/UAT draft directory, disabled in normal customer runs:
|
|
|
165
165
|
ask in chat only when the run is explicitly a non-interactive smoke/rehearsal.
|
|
166
166
|
|
|
167
167
|
- Customer-facing progress updates must use product language. Say "quick
|
|
168
|
-
setup choices", "campaign brief", "
|
|
168
|
+
setup choices", "campaign brief", "Find Leads", and "approval" instead of
|
|
169
169
|
`request_user_input`, Default mode, MCP namespaces, plugin caches, prompt
|
|
170
170
|
loading, runbooks, local skill files, skill versions, npm/package details,
|
|
171
171
|
repo-local files, VPS/off-desktop/browser automation limitations, or
|
|
@@ -181,6 +181,15 @@ Optional debug/UAT draft directory, disabled in normal customer runs:
|
|
|
181
181
|
or safe launch. Do not say "persist", "local draft folder", "artifact",
|
|
182
182
|
"mkdir", "campaign thesis", or "same approved campaign thesis" in
|
|
183
183
|
customer-facing progress copy.
|
|
184
|
+
- Watch guide copy must use the single `watchNarration` object whenever you set
|
|
185
|
+
`currentStep` or report visible progress in a watched run. Read
|
|
186
|
+
`references/watch-guide-narration.md` for the compact screen contract. Do not
|
|
187
|
+
send separate guide headline/body args. In Find Leads, the static stage label
|
|
188
|
+
is not enough: name the active lane/provider, what search or sample you are
|
|
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.
|
|
184
193
|
- Every approval gate must include live campaign access after the readable inline
|
|
185
194
|
content. Show a short `Watch link:` line with the campaign link when a
|
|
186
195
|
campaign shell exists. In normal customer runs, do not show `Open artifact:`
|
|
@@ -401,7 +410,7 @@ Optional debug/UAT draft directory, disabled in normal customer runs:
|
|
|
401
410
|
`RevOps`, `outbound systems`, or `pipeline architecture` unless they are
|
|
402
411
|
translated into what the user can understand.
|
|
403
412
|
|
|
404
|
-
After rendering that brief, ask for brief approval before
|
|
413
|
+
After rendering that brief, ask for brief approval before Find Leads in
|
|
405
414
|
hosted net-new runs. The user-facing choice should be approve/revise language,
|
|
406
415
|
not "looks good".
|
|
407
416
|
Approval options should refer to what the user just read, e.g. `Approve this
|
|
@@ -422,16 +431,15 @@ brief`, `Revise target`, `Revise offer/proof`, and `Other / custom`.
|
|
|
422
431
|
|
|
423
432
|
The approval question can be based on the rendered brief in chat and the live
|
|
424
433
|
campaign brief. Do not wait for file-write chrome before asking for approval.
|
|
425
|
-
Before
|
|
426
|
-
currentStep: "pick-provider" })` after approval so
|
|
427
|
-
Plan while the main thread compares source paths.
|
|
434
|
+
Before Find Leads, call `update_campaign({ campaignId, campaignBrief,
|
|
435
|
+
currentStep: "pick-provider", watchNarration: { ... } })` after approval so
|
|
436
|
+
the watch link moves out of Plan while the main thread compares source paths.
|
|
428
437
|
|
|
429
438
|
- After the brief is approved, show the next progress line:
|
|
430
439
|
`Cool. Now I'm going to find people who are both a good fit and active enough
|
|
431
440
|
to be worth a LinkedIn test. I'll compare source paths by expected volume,
|
|
432
441
|
sampled ICP fit, activity/warmth signal, cleanup risk, and tradeoffs. This
|
|
433
|
-
|
|
434
|
-
anything goes live.`
|
|
442
|
+
will end with a source decision + sample before anything goes live.`
|
|
435
443
|
- In watch mode, do not leave the user sitting on only `pick-provider` while
|
|
436
444
|
source scouts run. Move the campaign to the likely primary source lane
|
|
437
445
|
(`signal-discovery`, `sales-nav`, or `prospeo`) before background source
|
|
@@ -447,9 +455,8 @@ message we should test.`
|
|
|
447
455
|
- During long post-intake work, show concise progress checkpoints before the
|
|
448
456
|
next expensive stage: source being checked, source switch/tradeoff if any,
|
|
449
457
|
lead sample usable, filter/message drafting, and message-review rule loading.
|
|
450
|
-
Each checkpoint should
|
|
451
|
-
|
|
452
|
-
and explain why the extra care helps the campaign. Example:
|
|
458
|
+
Each checkpoint should say what changed, what is being checked next, and why
|
|
459
|
+
that matters for the campaign. Do not invent remaining-time estimates. Example:
|
|
453
460
|
|
|
454
461
|
```text
|
|
455
462
|
This is taking a little longer than I expected, sorry. I’m being careful here
|
|
@@ -575,6 +582,21 @@ workstreams`, `in parallel`, or `background` unless parallel branches were
|
|
|
575
582
|
`attach_recommended_sequence`, `attach_sequence`, and `start_campaign`.
|
|
576
583
|
`create_campaign`, `update_campaign`, and `save_rubrics` are allowed only when
|
|
577
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.
|
|
578
600
|
- During pre-import validation, do not call `check_rubric`; use the lead-filter
|
|
579
601
|
artifacts and only use campaign-backed scoring after Step 13 imports the
|
|
580
602
|
15-lead test batch.
|
|
@@ -623,7 +645,7 @@ workstreams`, `in parallel`, or `background` unless parallel branches were
|
|
|
623
645
|
There are four customer-visible gates in the net-new hosted flow:
|
|
624
646
|
|
|
625
647
|
- `brief-review` asks whether to approve the rendered campaign brief before
|
|
626
|
-
|
|
648
|
+
Find Leads starts. Do not skip this by inferring approval from setup
|
|
627
649
|
answers.
|
|
628
650
|
- `lead-review` / source decision asks whether to approve the selected source
|
|
629
651
|
path and sample before moving into filter/message work. Do not skip this by
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "v2",
|
|
3
3
|
"workflow": "create-campaign-v2",
|
|
4
|
-
"principle": "CampaignOffer state and the watch link are canonical from the first brief onward. Disk artifacts are optional debug/UAT diagnostics, not the normal customer surface. Resume, gating, and handoff read campaign state first. Start from user intake, create a watchable campaign shell with a v1 brief, update currentStep before major visible work, attach source/search state to that CampaignOffer, import the bounded review batch before post-import fit/message scouts, save rubrics and an approved message template, then queue the 15-row cascade and hand off to settings/sequence/start.",
|
|
4
|
+
"principle": "CampaignOffer state and the watch link are canonical from the first brief onward. Disk artifacts are optional debug/UAT diagnostics, not the normal customer surface. Resume, gating, and handoff read campaign state first. Start from user intake, create a watchable campaign shell with a v1 brief, update currentStep and watchNarration before major visible work, attach source/search state to that CampaignOffer, import the bounded review batch before post-import fit/message scouts, save rubrics and an approved message template, then queue the 15-row cascade and hand off to settings/sequence/start.",
|
|
5
5
|
"artifactPolicy": {
|
|
6
6
|
"normalCustomerPath": "Use CampaignOffer state, MCP responses, provider search state, and campaign table rows. Do not create, read, link, or surface local draft files unless debug/UAT mode is explicit or the user asks for them.",
|
|
7
7
|
"debugArtifactsAreOptional": true,
|
|
8
8
|
"requiredArtifactsApplyWhen": "legacy resume, fixture validation, or explicit debug/UAT mode only",
|
|
9
|
-
"customerFacingAccess": "Watch link
|
|
9
|
+
"customerFacingAccess": "Watch link, live campaign currentStep, and concise watchNarration"
|
|
10
10
|
},
|
|
11
11
|
"commitGateChoices": [
|
|
12
12
|
"approve",
|
|
@@ -225,9 +225,6 @@
|
|
|
225
225
|
"watch the draft campaign as it fills in",
|
|
226
226
|
"no leads import and nothing sends yet"
|
|
227
227
|
],
|
|
228
|
-
"timeEstimates": {
|
|
229
|
-
"campaignBrief": "~1-2 min"
|
|
230
|
-
},
|
|
231
228
|
"forbiddenWording": [
|
|
232
229
|
"brief.md",
|
|
233
230
|
"lead-review.md",
|
|
@@ -279,10 +276,12 @@
|
|
|
279
276
|
"name",
|
|
280
277
|
"campaignBrief",
|
|
281
278
|
"clientProspectId or senderLinkedinUrl",
|
|
282
|
-
"currentStep"
|
|
279
|
+
"currentStep",
|
|
280
|
+
"watchNarration"
|
|
283
281
|
],
|
|
284
282
|
"requiredValues": {
|
|
285
|
-
"currentStep": "create-offer"
|
|
283
|
+
"currentStep": "create-offer",
|
|
284
|
+
"watchNarration.stage": "brief-review"
|
|
286
285
|
},
|
|
287
286
|
"briefMode": "v1 campaign brief shown to the user; not an empty placeholder",
|
|
288
287
|
"capture": [
|
|
@@ -306,7 +305,8 @@
|
|
|
306
305
|
"campaignId",
|
|
307
306
|
"watchUrl",
|
|
308
307
|
"campaignBrief",
|
|
309
|
-
"currentStep:create-offer"
|
|
308
|
+
"currentStep:create-offer",
|
|
309
|
+
"watchNarration: brief-review with approve-the-brief guidance"
|
|
310
310
|
]
|
|
311
311
|
},
|
|
312
312
|
{
|
|
@@ -330,7 +330,7 @@
|
|
|
330
330
|
"campaignBrief"
|
|
331
331
|
],
|
|
332
332
|
"watchUrlSource": "create_campaign.watchUrl",
|
|
333
|
-
"requiredWatchUrlShape": "direct /campaign-builder/{campaignId}?mode=claude watch URL",
|
|
333
|
+
"requiredWatchUrlShape": "direct /campaign-builder/{campaignId}?mode={claude|codex} watch URL",
|
|
334
334
|
"codexBrowserHandoff": {
|
|
335
335
|
"openWhenAvailable": true,
|
|
336
336
|
"inspectBeforeContinuing": true,
|
|
@@ -444,7 +444,7 @@
|
|
|
444
444
|
],
|
|
445
445
|
"debugOrUatOnly": true,
|
|
446
446
|
"requiredBeforeTransition": false,
|
|
447
|
-
"reconcileRule": "If debug/UAT artifacts are enabled, verify the sidecar persisted the same brief shown in chat. Normal customer
|
|
447
|
+
"reconcileRule": "If debug/UAT artifacts are enabled, verify the sidecar persisted the same brief shown in chat. Normal customer Find Leads work reads CampaignOffer.campaignBrief, not brief.md.",
|
|
448
448
|
"chatRenderRule": "Do not block the brief approval question or find-leads transition on this step. Only mention artifact persistence if debug/UAT mode fails or the user asks."
|
|
449
449
|
},
|
|
450
450
|
{
|
|
@@ -457,7 +457,8 @@
|
|
|
457
457
|
"when": "after_user_brief_confirmed_or_auto_continue",
|
|
458
458
|
"fields": [
|
|
459
459
|
"campaignBrief",
|
|
460
|
-
"currentStep:pick-provider"
|
|
460
|
+
"currentStep:pick-provider",
|
|
461
|
+
"watchNarration: find-leads with the first provider/lane being tested"
|
|
461
462
|
],
|
|
462
463
|
"fallback": "If campaignId is missing from CampaignOffer state, stop; shell-first flow requires an existing campaign before sourcing leads.",
|
|
463
464
|
"readsCampaignStateFirst": true,
|
|
@@ -512,20 +513,23 @@
|
|
|
512
513
|
"tool": "update_campaign",
|
|
513
514
|
"requiredFields": [
|
|
514
515
|
"campaignId",
|
|
515
|
-
"currentStep"
|
|
516
|
+
"currentStep",
|
|
517
|
+
"watchNarration"
|
|
516
518
|
],
|
|
517
519
|
"requiredValues": {
|
|
518
|
-
"currentStep": "pick-provider"
|
|
520
|
+
"currentStep": "pick-provider",
|
|
521
|
+
"watchNarration.stage": "find-leads"
|
|
519
522
|
},
|
|
520
523
|
"when": "before_source_scouts_or_provider_search",
|
|
521
|
-
"chatRenderRule": "Move the campaign watch view to Find
|
|
524
|
+
"chatRenderRule": "Move the campaign watch view to Find Leads before the main thread starts comparing source paths. The watchNarration headline/body must name the likely provider or lane being tested and why it fits this campaign. Do not mention MCP or local artifacts."
|
|
522
525
|
},
|
|
523
526
|
{
|
|
524
527
|
"action": "advance_watch_to_initial_source_lane",
|
|
525
528
|
"tool": "update_campaign",
|
|
526
529
|
"requiredFields": [
|
|
527
530
|
"campaignId",
|
|
528
|
-
"currentStep"
|
|
531
|
+
"currentStep",
|
|
532
|
+
"watchNarration"
|
|
529
533
|
],
|
|
530
534
|
"currentStepByPrimaryLane": {
|
|
531
535
|
"signals": "signal-discovery",
|
|
@@ -536,7 +540,7 @@
|
|
|
536
540
|
"uploadedDomains": "prospeo"
|
|
537
541
|
},
|
|
538
542
|
"when": "before_background_source_scouts",
|
|
539
|
-
"rule": "Choose the likely primary visible source lane from source intake, brief preference, or the best first lane the main thread will actually search. Do this before waiting on background scouts so watch mode never sits on only Pick Provider while source work happens elsewhere."
|
|
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."
|
|
540
544
|
},
|
|
541
545
|
{
|
|
542
546
|
"action": "run_first_campaign_attached_source_search",
|
|
@@ -565,7 +569,7 @@
|
|
|
565
569
|
"source decision + sample",
|
|
566
570
|
"no import or enrichment yet"
|
|
567
571
|
],
|
|
568
|
-
"
|
|
572
|
+
"watchNarrationRule": "Do not include time estimates. Use current, campaign-specific search reasoning instead."
|
|
569
573
|
},
|
|
570
574
|
{
|
|
571
575
|
"action": "read_optional_source_intake",
|
|
@@ -740,6 +744,7 @@
|
|
|
740
744
|
"artifactLinkTiming": "before_next_step_or_revision_question",
|
|
741
745
|
"doNotCompressToSummaryOnly": false,
|
|
742
746
|
"doNotRenderArtifactLinksOnly": true,
|
|
747
|
+
"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.",
|
|
743
748
|
"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."
|
|
744
749
|
},
|
|
745
750
|
{
|
|
@@ -755,7 +760,8 @@
|
|
|
755
760
|
"import the first 15-row review batch before fit/message work",
|
|
756
761
|
"selectedLeadListId stays the source list and workflowTableId is the campaign table"
|
|
757
762
|
],
|
|
758
|
-
"
|
|
763
|
+
"watchNarrationRule": "Do not include time estimates. Say what review-batch work is happening and what the user will approve next.",
|
|
764
|
+
"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.",
|
|
759
765
|
"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."
|
|
760
766
|
},
|
|
761
767
|
{
|
|
@@ -786,6 +792,7 @@
|
|
|
786
792
|
"leadSourceProvider",
|
|
787
793
|
"providerSearchAssociation",
|
|
788
794
|
"currentStep: primary provider step (signal-discovery, sales-nav, prospeo/contact-search, saved-lists, or leads)",
|
|
795
|
+
"watchNarration: find-leads source recommendation ready, or review-batch import starting when source approval already happened",
|
|
789
796
|
"selectedLeadListId as source list id only for existing-list or supplied-list preview"
|
|
790
797
|
],
|
|
791
798
|
"fallback": "Stop if campaignId is missing; the source must be attached to the existing CampaignOffer before import.",
|
|
@@ -866,7 +873,8 @@
|
|
|
866
873
|
"onMissingCampaignAttachedSource": "stop_before_import_and_route_to_find_leads; source scouts must attach searches/selections with campaignOfferId before Step 13"
|
|
867
874
|
},
|
|
868
875
|
{
|
|
869
|
-
"action": "watch_mode_orient"
|
|
876
|
+
"action": "watch_mode_orient",
|
|
877
|
+
"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."
|
|
870
878
|
},
|
|
871
879
|
{
|
|
872
880
|
"tool": "import_leads",
|
|
@@ -929,10 +937,12 @@
|
|
|
929
937
|
"tool": "update_campaign",
|
|
930
938
|
"requiredFields": [
|
|
931
939
|
"campaignId",
|
|
932
|
-
"currentStep"
|
|
940
|
+
"currentStep",
|
|
941
|
+
"watchNarration"
|
|
933
942
|
],
|
|
934
943
|
"requiredValues": {
|
|
935
|
-
"currentStep": "filter-choice"
|
|
944
|
+
"currentStep": "filter-choice",
|
|
945
|
+
"watchNarration.stage": "fit-message"
|
|
936
946
|
}
|
|
937
947
|
}
|
|
938
948
|
],
|
|
@@ -1033,17 +1043,17 @@
|
|
|
1033
1043
|
"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."
|
|
1034
1044
|
},
|
|
1035
1045
|
{
|
|
1036
|
-
"action": "
|
|
1046
|
+
"action": "wait_for_lead_filter_artifact",
|
|
1037
1047
|
"requiredArtifacts": [
|
|
1038
|
-
"lead-filter.md"
|
|
1039
|
-
"message-validation.md"
|
|
1048
|
+
"lead-filter.md"
|
|
1040
1049
|
],
|
|
1041
1050
|
"optionalArtifacts": [
|
|
1042
1051
|
"rubric.json",
|
|
1052
|
+
"message-validation.md",
|
|
1043
1053
|
"message-prep.md",
|
|
1044
1054
|
"message-candidate-drafts.md"
|
|
1045
1055
|
],
|
|
1046
|
-
"
|
|
1056
|
+
"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."
|
|
1047
1057
|
},
|
|
1048
1058
|
{
|
|
1049
1059
|
"action": "save_filter_rubrics_to_campaign",
|
|
@@ -1063,6 +1073,39 @@
|
|
|
1063
1073
|
],
|
|
1064
1074
|
"writesCampaignState": "leadScoringRubrics",
|
|
1065
1075
|
"requiredBeforeCascade": true
|
|
1076
|
+
},
|
|
1077
|
+
{
|
|
1078
|
+
"action": "advance_watch_to_filter_leads_after_rubrics_saved",
|
|
1079
|
+
"tool": "update_campaign",
|
|
1080
|
+
"requires": [
|
|
1081
|
+
"campaignId",
|
|
1082
|
+
"workflowTableId",
|
|
1083
|
+
"leadScoringRubrics"
|
|
1084
|
+
],
|
|
1085
|
+
"requiredValues": {
|
|
1086
|
+
"currentStep": "apply-icp-rubric",
|
|
1087
|
+
"enableICPFilters": true,
|
|
1088
|
+
"watchNarration.stage": "fit-message"
|
|
1089
|
+
},
|
|
1090
|
+
"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.",
|
|
1091
|
+
"when": "after_save_rubrics_succeeds_before_waiting_for_message_validation",
|
|
1092
|
+
"writesCampaignState": "currentStep:apply-icp-rubric"
|
|
1093
|
+
},
|
|
1094
|
+
{
|
|
1095
|
+
"tool": "get_campaign_navigation_state",
|
|
1096
|
+
"purpose": "confirm the watched UI moved to Filter Leads after rubrics saved",
|
|
1097
|
+
"optional": true
|
|
1098
|
+
},
|
|
1099
|
+
{
|
|
1100
|
+
"action": "wait_for_message_validation_artifact",
|
|
1101
|
+
"requiredArtifacts": [
|
|
1102
|
+
"message-validation.md"
|
|
1103
|
+
],
|
|
1104
|
+
"optionalArtifacts": [
|
|
1105
|
+
"message-prep.md",
|
|
1106
|
+
"message-candidate-drafts.md"
|
|
1107
|
+
],
|
|
1108
|
+
"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."
|
|
1066
1109
|
}
|
|
1067
1110
|
],
|
|
1068
1111
|
"requiredArtifacts": [
|
|
@@ -1084,6 +1127,8 @@
|
|
|
1084
1127
|
"get_subskill_asset",
|
|
1085
1128
|
"get_post_find_leads_scout_registry",
|
|
1086
1129
|
"save_rubrics",
|
|
1130
|
+
"update_campaign",
|
|
1131
|
+
"get_campaign_navigation_state",
|
|
1087
1132
|
"Task",
|
|
1088
1133
|
"spawn_agent",
|
|
1089
1134
|
"AskUserQuestion",
|
|
@@ -1100,7 +1145,6 @@
|
|
|
1100
1145
|
"create_campaign",
|
|
1101
1146
|
"import_leads",
|
|
1102
1147
|
"confirm_lead_list",
|
|
1103
|
-
"update_campaign",
|
|
1104
1148
|
"queue_cells",
|
|
1105
1149
|
"start_campaign",
|
|
1106
1150
|
"check_rubric",
|
|
@@ -1108,7 +1152,7 @@
|
|
|
1108
1152
|
"enrich_with_prospeo",
|
|
1109
1153
|
"bulk_enrich_with_prospeo"
|
|
1110
1154
|
],
|
|
1111
|
-
"watchRequired":
|
|
1155
|
+
"watchRequired": true,
|
|
1112
1156
|
"waitFor": [
|
|
1113
1157
|
"post_lead_workstreams_ready",
|
|
1114
1158
|
"revise_leads",
|
|
@@ -1489,7 +1533,10 @@
|
|
|
1489
1533
|
"message-review-decision.md"
|
|
1490
1534
|
],
|
|
1491
1535
|
"requiredValues": {
|
|
1492
|
-
"currentStep": "validate-sample"
|
|
1536
|
+
"currentStep": "validate-sample",
|
|
1537
|
+
"enableICPFilters": true,
|
|
1538
|
+
"useMessagingTemplate": true,
|
|
1539
|
+
"watchNarration.stage": "review-ready"
|
|
1493
1540
|
},
|
|
1494
1541
|
"when": "after_update_campaign_brief_succeeds",
|
|
1495
1542
|
"requiredBeforeCascade": true,
|
|
@@ -1846,7 +1893,7 @@
|
|
|
1846
1893
|
"Campaign created",
|
|
1847
1894
|
"approved brief",
|
|
1848
1895
|
"Open this to watch",
|
|
1849
|
-
"
|
|
1896
|
+
"Find Leads",
|
|
1850
1897
|
"rubric scoring",
|
|
1851
1898
|
"messaging populate live",
|
|
1852
1899
|
"Watch link:"
|
|
@@ -1932,7 +1979,7 @@
|
|
|
1932
1979
|
},
|
|
1933
1980
|
{
|
|
1934
1981
|
"tool": "wait_for_campaign_table_ready",
|
|
1935
|
-
"purpose": "
|
|
1982
|
+
"purpose": "wait_for_review_batch_cascade_to_start_returning_filter_results"
|
|
1936
1983
|
},
|
|
1937
1984
|
{
|
|
1938
1985
|
"tool": "get_rows_minimal",
|
|
@@ -1947,11 +1994,12 @@
|
|
|
1947
1994
|
"minPassedCount"
|
|
1948
1995
|
],
|
|
1949
1996
|
"targetCountSource": "stats.totalRows_or_imported_batch_count",
|
|
1950
|
-
"minPassedCountSource": "
|
|
1997
|
+
"minPassedCountSource": "firstPassingRowForMessageStart (1)",
|
|
1951
1998
|
"requiredValues": {
|
|
1952
|
-
"includeRows": false
|
|
1999
|
+
"includeRows": false,
|
|
2000
|
+
"minPassedCount": 1
|
|
1953
2001
|
},
|
|
1954
|
-
"note": "The shell-first flow tests 15 leads first; always pass cohortSize explicitly instead of relying on default 25 behavior. Pass minPassedCount so
|
|
2002
|
+
"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.",
|
|
1955
2003
|
"readVia": "stats_only_tool_result",
|
|
1956
2004
|
"extractFields": [
|
|
1957
2005
|
"ready",
|
|
@@ -1968,7 +2016,7 @@
|
|
|
1968
2016
|
{
|
|
1969
2017
|
"action": "handle_partial_or_timeout_sample",
|
|
1970
2018
|
"when": "wait_for_rubric_results.ready_false_or_reason_timeout",
|
|
1971
|
-
"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
|
|
2019
|
+
"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.",
|
|
1972
2020
|
"customerStatus": "sample-needs-revision",
|
|
1973
2021
|
"showFields": [
|
|
1974
2022
|
"passRate.completed",
|
|
@@ -2017,6 +2065,7 @@
|
|
|
2017
2065
|
"revision_round_persists_across_resume",
|
|
2018
2066
|
"wait_for_rubric_results_never_retain_rows_payload_in_tail_context",
|
|
2019
2067
|
"wait_for_rubric_results_targetCount_always_explicit",
|
|
2068
|
+
"first_passing_row_unblocks_generate_message_observation",
|
|
2020
2069
|
"timeout_never_repeats_without_customer_handoff",
|
|
2021
2070
|
"timeout_or_underfloor_sample_never_advances_to_settings",
|
|
2022
2071
|
"review_batch_cascade_waits_for_saved_rubrics_and_approved_template"
|
|
@@ -2118,10 +2167,12 @@
|
|
|
2118
2167
|
"tool": "update_campaign",
|
|
2119
2168
|
"requiredFields": [
|
|
2120
2169
|
"campaignId",
|
|
2121
|
-
"currentStep"
|
|
2170
|
+
"currentStep",
|
|
2171
|
+
"watchNarration"
|
|
2122
2172
|
],
|
|
2123
2173
|
"requiredValues": {
|
|
2124
|
-
"currentStep": "awaiting-user-greenlight"
|
|
2174
|
+
"currentStep": "awaiting-user-greenlight",
|
|
2175
|
+
"watchNarration.stage": "review-ready"
|
|
2125
2176
|
}
|
|
2126
2177
|
}
|
|
2127
2178
|
],
|
|
@@ -2188,7 +2239,7 @@
|
|
|
2188
2239
|
},
|
|
2189
2240
|
{
|
|
2190
2241
|
"action": "surface_sender_and_slack_handoff",
|
|
2191
|
-
"settingsUrlPattern": "/campaign-builder/{campaignId}/settings?mode=claude",
|
|
2242
|
+
"settingsUrlPattern": "/campaign-builder/{campaignId}/settings?mode={claude|codex}",
|
|
2192
2243
|
"requiredVisibleContent": [
|
|
2193
2244
|
"connect or select a LinkedIn sender",
|
|
2194
2245
|
"Slack reply review",
|
|
@@ -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
|
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Watch Guide Narration
|
|
2
|
+
|
|
3
|
+
Use one `watchNarration` object whenever you set `currentStep` or report visible
|
|
4
|
+
progress in a watched create-campaign run. Do not add separate guide headline or
|
|
5
|
+
body args.
|
|
6
|
+
|
|
7
|
+
## Render
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
Building campaign with {Claude Code or Codex} [Exit watch mode]
|
|
11
|
+
[progress bar] {stage label} · Step {n} of 5
|
|
12
|
+
|
|
13
|
+
{headline}
|
|
14
|
+
{visibleState} {agentIntent}
|
|
15
|
+
|
|
16
|
+
Next: {nextAction}
|
|
17
|
+
{safety?}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
UI owns the header, driver label, button, progress bar, step count, and static
|
|
21
|
+
stage labels. The driver label comes from the watch URL mode (`mode=claude` or
|
|
22
|
+
`mode=codex`), so do not add separate guide args for it. You own `stage`,
|
|
23
|
+
`headline`, `visibleState`, `agentIntent`, and any exceptional `nextAction`,
|
|
24
|
+
`safety`, `workerStatuses`, or `blockedReason`.
|
|
25
|
+
|
|
26
|
+
## Copy Rules
|
|
27
|
+
|
|
28
|
+
- One primary user action per frame.
|
|
29
|
+
- Headline under 8 words.
|
|
30
|
+
- Body is 1-2 short sentences total.
|
|
31
|
+
- Say where the user acts: usually `in Claude Code`, `in Codex`, or `in chat`,
|
|
32
|
+
matching the host that created the watch URL.
|
|
33
|
+
- `Find Leads` is only the static progress label. The copy must name the active
|
|
34
|
+
lane/provider and campaign strategy: Signal Discovery, Sales Nav, Prospeo,
|
|
35
|
+
Apollo, or existing list.
|
|
36
|
+
- During search iteration, say what you tried, what you are trying next, what
|
|
37
|
+
sample you are checking, and why that helps this campaign.
|
|
38
|
+
- Avoid internal terms: MCP, tool, currentStep, workflow table, scout, debug.
|
|
39
|
+
- Do not invent time estimates.
|
|
40
|
+
|
|
41
|
+
## Stages
|
|
42
|
+
|
|
43
|
+
| stage | UI label | Next |
|
|
44
|
+
| -------------- | ------------- | ------------- |
|
|
45
|
+
| `brief-review` | Brief review | Find Leads |
|
|
46
|
+
| `find-leads` | Find Leads | Review batch |
|
|
47
|
+
| `review-batch` | Review batch | Fit + message |
|
|
48
|
+
| `fit-message` | Fit + message | Review ready |
|
|
49
|
+
| `review-ready` | Review ready | Validation |
|
|
50
|
+
|
|
51
|
+
## Examples
|
|
52
|
+
|
|
53
|
+
Brief review:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"stage": "brief-review",
|
|
58
|
+
"headline": "Approve the brief in Codex",
|
|
59
|
+
"visibleState": "This page is a live preview of the campaign brief.",
|
|
60
|
+
"agentIntent": "Ask for changes in chat if anything looks off.",
|
|
61
|
+
"nextAction": "Find Leads"
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Signal Discovery:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"stage": "find-leads",
|
|
70
|
+
"headline": "Testing Signal Discovery",
|
|
71
|
+
"visibleState": "You are watching Signal Discovery search posts about Claude Code and GTM automation.",
|
|
72
|
+
"agentIntent": "Codex is sampling engagers to see if they match the founder/operator campaign brief.",
|
|
73
|
+
"nextAction": "Review batch"
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Search iteration:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"stage": "find-leads",
|
|
82
|
+
"headline": "Tightening the search",
|
|
83
|
+
"visibleState": "The first Signal Discovery search was broad, so Codex is trying a narrower Claude Code founder lane.",
|
|
84
|
+
"agentIntent": "It is sampling a few engagers before importing so the review batch stays on-brief.",
|
|
85
|
+
"nextAction": "Review batch"
|
|
86
|
+
}
|
|
87
|
+
```
|
|
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
|
+
|
|
107
|
+
Review batch:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"stage": "review-batch",
|
|
112
|
+
"headline": "Preparing your review batch",
|
|
113
|
+
"visibleState": "This page shows the leads Codex selected for review.",
|
|
114
|
+
"agentIntent": "Codex is preparing the first campaign batch from those leads.",
|
|
115
|
+
"nextAction": "Fit + message"
|
|
116
|
+
}
|
|
117
|
+
```
|
|
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
|
+
|
|
137
|
+
Fit + message:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"stage": "fit-message",
|
|
142
|
+
"headline": "Building fit and message",
|
|
143
|
+
"visibleState": "Codex is preparing the campaign's fit rules and first message sample.",
|
|
144
|
+
"agentIntent": "It is checking who should receive this campaign and what the first offer should say.",
|
|
145
|
+
"nextAction": "Review ready",
|
|
146
|
+
"workerStatuses": {
|
|
147
|
+
"leadFitBuilder": "running",
|
|
148
|
+
"messageDraftBuilder": "running"
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Review ready:
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"stage": "review-ready",
|
|
158
|
+
"headline": "Review fit and message",
|
|
159
|
+
"visibleState": "The fit rules and message sample are ready.",
|
|
160
|
+
"agentIntent": "Approve them or ask for changes in chat.",
|
|
161
|
+
"nextAction": "Validation",
|
|
162
|
+
"safety": "Codex will only continue after you approve."
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Blocked/recovering:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"stage": "review-batch",
|
|
171
|
+
"headline": "Fixing the review batch",
|
|
172
|
+
"visibleState": "The browser is not showing the campaign batch yet.",
|
|
173
|
+
"agentIntent": "Codex is reconnecting the campaign state before moving forward.",
|
|
174
|
+
"nextAction": "Fit + message",
|
|
175
|
+
"blockedReason": "Campaign batch is not ready yet."
|
|
176
|
+
}
|
|
177
|
+
```
|
|
@@ -45,7 +45,7 @@ Cool, let's find leads.
|
|
|
45
45
|
Atomic-mint legacy orientation:
|
|
46
46
|
|
|
47
47
|
```text
|
|
48
|
-
Open this to watch
|
|
48
|
+
Open this to watch Find Leads, rubric scoring, and messaging populate live.
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
The exact wording may be adapted for tone, but the copy must make three things
|
|
@@ -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
|