@sellable/mcp 0.1.110 → 0.1.112
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 +68 -8
- package/dist/tools/context.d.ts +1 -1
- package/dist/tools/context.js +1 -1
- package/dist/tools/leads.d.ts +5 -0
- package/dist/tools/leads.js +66 -3
- package/dist/tools/navigation.d.ts +2 -2
- package/dist/tools/navigation.js +27 -14
- package/dist/tools/readiness.d.ts +3 -3
- package/dist/tools/watch-url-security.js +18 -3
- package/package.json +1 -1
- package/skills/create-campaign/SKILL.md +10 -10
- package/skills/create-campaign-v2/SKILL.md +16 -11
- package/skills/create-campaign-v2/core/flow.v2.json +34 -25
- package/skills/create-campaign-v2/references/watch-guide-narration.md +141 -0
- package/skills/create-campaign-v2/references/watch-link-handoff.md +11 -11
|
@@ -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
|
@@ -21,10 +21,20 @@ function normalizeLeadSourceProvider(input) {
|
|
|
21
21
|
}
|
|
22
22
|
export function buildWatchUrl(config, path) {
|
|
23
23
|
const workspaceId = config.activeWorkspaceId || config.workspaceId;
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
const url = new URL(path, config.apiUrl);
|
|
25
|
+
if (workspaceId && !url.searchParams.has("workspaceId")) {
|
|
26
|
+
url.searchParams.set("workspaceId", workspaceId);
|
|
27
|
+
}
|
|
28
|
+
return url.toString();
|
|
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()}`;
|
|
28
38
|
}
|
|
29
39
|
function isLinkedInProfileUrl(input) {
|
|
30
40
|
try {
|
|
@@ -175,6 +185,31 @@ export const campaignToolDefinitions = [
|
|
|
175
185
|
type: ["string", "null"],
|
|
176
186
|
description: "Workflow step ID (headless or UI step ID such as filter-rules)",
|
|
177
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
|
+
},
|
|
178
213
|
leadSourceType: {
|
|
179
214
|
type: ["string", "null"],
|
|
180
215
|
description: "Lead source type (existing | new)",
|
|
@@ -243,10 +278,35 @@ export const campaignToolDefinitions = [
|
|
|
243
278
|
type: ["string", "null"],
|
|
244
279
|
description: "Headless workflow step ID",
|
|
245
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
|
+
},
|
|
246
306
|
interactionMode: {
|
|
247
307
|
type: "string",
|
|
248
308
|
enum: ["step-by-step", "autonomous", "ask-when-needed"],
|
|
249
|
-
description: "Execution style for
|
|
309
|
+
description: "Execution style for Find Leads steps (step-by-step | autonomous | ask-when-needed).",
|
|
250
310
|
},
|
|
251
311
|
enableICPFilters: {
|
|
252
312
|
type: ["boolean", "null"],
|
|
@@ -334,7 +394,7 @@ export async function getCampaign(campaignId) {
|
|
|
334
394
|
api.get(`/api/v3/mcp/campaigns/${campaignId}`),
|
|
335
395
|
fetchCampaignRubrics(campaignId).catch(() => null),
|
|
336
396
|
]);
|
|
337
|
-
const watchUrl = buildWatchUrl(config,
|
|
397
|
+
const watchUrl = buildWatchUrl(config, buildCampaignBuilderWatchPath(campaignId));
|
|
338
398
|
// Merge rubrics into campaignOffer
|
|
339
399
|
const rubrics = (rubricsResult?.rubrics || []).map((r) => ({
|
|
340
400
|
id: r.id || "",
|
|
@@ -589,7 +649,7 @@ export async function createCampaign(input) {
|
|
|
589
649
|
// Idempotent resume path
|
|
590
650
|
if (input.campaignId) {
|
|
591
651
|
const existing = await api.get(`/api/v2/campaign-offers/${input.campaignId}`);
|
|
592
|
-
const watchUrl = buildWatchUrl(config,
|
|
652
|
+
const watchUrl = buildWatchUrl(config, buildCampaignBuilderWatchPath(existing.id));
|
|
593
653
|
const hasCreateFields = input.name ||
|
|
594
654
|
input.clientProspectId ||
|
|
595
655
|
input.offerPositioning !== undefined ||
|
|
@@ -707,7 +767,7 @@ export async function createCampaign(input) {
|
|
|
707
767
|
},
|
|
708
768
|
};
|
|
709
769
|
const result = await api.post(`/api/v2/campaign-offers`, formattedInput);
|
|
710
|
-
const watchUrl = buildWatchUrl(config,
|
|
770
|
+
const watchUrl = buildWatchUrl(config, buildCampaignBuilderWatchPath(result.id));
|
|
711
771
|
// Serialize to only essential fields for context efficiency
|
|
712
772
|
return {
|
|
713
773
|
id: result.id,
|
package/dist/tools/context.d.ts
CHANGED
package/dist/tools/context.js
CHANGED
package/dist/tools/leads.d.ts
CHANGED
|
@@ -2460,6 +2460,11 @@ export declare function confirmLeadList(input: ConfirmLeadListInput): Promise<{
|
|
|
2460
2460
|
rowIds?: string[];
|
|
2461
2461
|
requestedTargetLeadCount?: number;
|
|
2462
2462
|
sourceRowLimit?: number;
|
|
2463
|
+
async?: boolean;
|
|
2464
|
+
campaignTableReady?: boolean;
|
|
2465
|
+
importStatus?: string | null;
|
|
2466
|
+
remainingRowCount?: number | null;
|
|
2467
|
+
rowCount?: number | null;
|
|
2463
2468
|
};
|
|
2464
2469
|
boundedReviewBatch: {
|
|
2465
2470
|
requestedTargetLeadCount: number;
|
package/dist/tools/leads.js
CHANGED
|
@@ -2199,7 +2199,30 @@ export async function confirmLeadList(input) {
|
|
|
2199
2199
|
if (!resolvedLeadListId) {
|
|
2200
2200
|
throw new Error("confirm_lead_list requires sourceLeadListId");
|
|
2201
2201
|
}
|
|
2202
|
-
const
|
|
2202
|
+
const safePostConfirmCurrentSteps = new Set([
|
|
2203
|
+
"filter-choice",
|
|
2204
|
+
"create-icp-rubric",
|
|
2205
|
+
"apply-icp-rubric",
|
|
2206
|
+
"validate-sample",
|
|
2207
|
+
"auto-execute-leads",
|
|
2208
|
+
"messages",
|
|
2209
|
+
"auto-execute-messaging",
|
|
2210
|
+
"awaiting-user-greenlight",
|
|
2211
|
+
"settings",
|
|
2212
|
+
"sequence",
|
|
2213
|
+
"send",
|
|
2214
|
+
"claude-greenlight",
|
|
2215
|
+
"running",
|
|
2216
|
+
]);
|
|
2217
|
+
const effectiveCurrentStep = currentStep === undefined
|
|
2218
|
+
? "filter-choice"
|
|
2219
|
+
: typeof currentStep === "string" &&
|
|
2220
|
+
currentStep.length > 0 &&
|
|
2221
|
+
safePostConfirmCurrentSteps.has(currentStep)
|
|
2222
|
+
? currentStep
|
|
2223
|
+
: currentStep === null
|
|
2224
|
+
? null
|
|
2225
|
+
: "filter-choice";
|
|
2203
2226
|
const shouldSetCurrentStep = typeof effectiveCurrentStep === "string" && effectiveCurrentStep.length > 0;
|
|
2204
2227
|
let readiness;
|
|
2205
2228
|
const leadListMeta = await api.get(`/api/v3/workflow-tables/${resolvedLeadListId}?mode=meta`);
|
|
@@ -2257,13 +2280,27 @@ export async function confirmLeadList(input) {
|
|
|
2257
2280
|
}
|
|
2258
2281
|
throw new Error("Import still in progress. Keep the campaign at confirm-lead-list; retry readiness, cancel the import, or re-run-source before launching post-import scouts.");
|
|
2259
2282
|
}
|
|
2260
|
-
const
|
|
2283
|
+
const isTerminalAccessError = (error) => error instanceof SellableApiError && [401, 403, 404].includes(error.status);
|
|
2284
|
+
const formatTerminalAccessError = (error) => {
|
|
2285
|
+
if (error.status === 401 || error.status === 403) {
|
|
2286
|
+
return "Campaign or lead-list access failed. Fix workspace/auth access before retrying confirm_lead_list.";
|
|
2287
|
+
}
|
|
2288
|
+
return "Campaign or source lead list could not be found. Re-open the correct campaign or reselect the source list before retrying confirm_lead_list.";
|
|
2289
|
+
};
|
|
2290
|
+
const importResult = await api
|
|
2291
|
+
.post(`/api/v3/campaign-builder/import-leads`, {
|
|
2261
2292
|
sourceLeadListId: resolvedLeadListId,
|
|
2262
2293
|
campaignOfferId,
|
|
2263
2294
|
campaignName,
|
|
2264
2295
|
keepInSync,
|
|
2265
2296
|
...(typeof targetLeadCount === "number" ? { targetLeadCount } : {}),
|
|
2266
2297
|
...(shouldSetCurrentStep ? { currentStep: effectiveCurrentStep } : {}),
|
|
2298
|
+
})
|
|
2299
|
+
.catch((error) => {
|
|
2300
|
+
if (isTerminalAccessError(error)) {
|
|
2301
|
+
throw new Error(formatTerminalAccessError(error));
|
|
2302
|
+
}
|
|
2303
|
+
throw error;
|
|
2267
2304
|
});
|
|
2268
2305
|
const campaignTableId = importResult.workflowTableId ?? importResult.campaignTableId;
|
|
2269
2306
|
const requestedTargetLeadCount = typeof targetLeadCount === "number" && Number.isFinite(targetLeadCount)
|
|
@@ -2275,6 +2312,29 @@ export async function confirmLeadList(input) {
|
|
|
2275
2312
|
const overflowRowIds = campaignTableId && requestedTargetLeadCount !== null
|
|
2276
2313
|
? importedRowIds.slice(requestedTargetLeadCount)
|
|
2277
2314
|
: [];
|
|
2315
|
+
const importedRowCount = importedRowIds.length > 0
|
|
2316
|
+
? importedRowIds.length
|
|
2317
|
+
: typeof importResult.rowCount === "number"
|
|
2318
|
+
? importResult.rowCount
|
|
2319
|
+
: typeof importResult.leadsImported === "number"
|
|
2320
|
+
? importResult.leadsImported
|
|
2321
|
+
: 0;
|
|
2322
|
+
const remainingRowCount = typeof importResult.remainingRowCount === "number"
|
|
2323
|
+
? importResult.remainingRowCount
|
|
2324
|
+
: 0;
|
|
2325
|
+
const importStatus = String(importResult.importStatus ?? "").toLowerCase();
|
|
2326
|
+
const campaignTableReady = importResult.campaignTableReady === true ||
|
|
2327
|
+
(!importResult.async &&
|
|
2328
|
+
importStatus !== "pending" &&
|
|
2329
|
+
importStatus !== "partial" &&
|
|
2330
|
+
remainingRowCount <= 0 &&
|
|
2331
|
+
importedRowCount > 0);
|
|
2332
|
+
if (!campaignTableReady) {
|
|
2333
|
+
throw new Error("Campaign review rows are still importing. Stay on lead import until the bounded campaign table is ready, then retry confirm_lead_list or wait_for_campaign_table_ready.");
|
|
2334
|
+
}
|
|
2335
|
+
if (importedRowCount <= 0) {
|
|
2336
|
+
throw new Error("No usable review rows were kept for the campaign table. Tighten or change the source before continuing.");
|
|
2337
|
+
}
|
|
2278
2338
|
if (campaignTableId && overflowRowIds.length > 0) {
|
|
2279
2339
|
const deleteBatchSize = 25;
|
|
2280
2340
|
for (let index = 0; index < overflowRowIds.length; index += deleteBatchSize) {
|
|
@@ -2310,7 +2370,10 @@ export async function confirmLeadList(input) {
|
|
|
2310
2370
|
trimmedOverflowRowCount: overflowRowIds.length,
|
|
2311
2371
|
}
|
|
2312
2372
|
: undefined,
|
|
2313
|
-
message:
|
|
2373
|
+
message: requestedTargetLeadCount !== null &&
|
|
2374
|
+
leadListRowCount > requestedTargetLeadCount
|
|
2375
|
+
? `I found ${leadListRowCount} source candidates and imported the first ${Math.min(importedRowCount, requestedTargetLeadCount)} into the campaign review table.`
|
|
2376
|
+
: "Lead list imported into the campaign review table. Next: sample rows with get_rows_minimal or continue into filter work.",
|
|
2314
2377
|
};
|
|
2315
2378
|
}
|
|
2316
2379
|
export function getProviderPrompt(input) {
|
|
@@ -81,7 +81,7 @@ export declare function computeCampaignNavigationStateFromCampaign(campaign: Cam
|
|
|
81
81
|
};
|
|
82
82
|
watchUrl: {
|
|
83
83
|
urlPresent: boolean;
|
|
84
|
-
|
|
84
|
+
direct: boolean | undefined;
|
|
85
85
|
redirectPath: string | null;
|
|
86
86
|
};
|
|
87
87
|
table: {
|
|
@@ -125,7 +125,7 @@ export declare function getCampaignNavigationState(input: GetCampaignNavigationS
|
|
|
125
125
|
};
|
|
126
126
|
watchUrl: {
|
|
127
127
|
urlPresent: boolean;
|
|
128
|
-
|
|
128
|
+
direct: boolean | undefined;
|
|
129
129
|
redirectPath: string | null;
|
|
130
130
|
};
|
|
131
131
|
table: {
|
package/dist/tools/navigation.js
CHANGED
|
@@ -281,9 +281,9 @@ function mapHeadlessToUiStep(step) {
|
|
|
281
281
|
return "signal-discovery";
|
|
282
282
|
if (step === "filter-choice")
|
|
283
283
|
return "filter-choice";
|
|
284
|
-
if (step === "create-icp-rubric"
|
|
285
|
-
|
|
286
|
-
|
|
284
|
+
if (step === "create-icp-rubric" ||
|
|
285
|
+
step === "apply-icp-rubric" ||
|
|
286
|
+
step === "validate-sample") {
|
|
287
287
|
return "filter-leads";
|
|
288
288
|
}
|
|
289
289
|
if (step === "messages" ||
|
|
@@ -381,7 +381,7 @@ function describeVisibleStep(step) {
|
|
|
381
381
|
};
|
|
382
382
|
}
|
|
383
383
|
}
|
|
384
|
-
function
|
|
384
|
+
function parseWatchUrl(watchUrl, campaignId) {
|
|
385
385
|
if (!watchUrl) {
|
|
386
386
|
return {
|
|
387
387
|
urlPresent: false,
|
|
@@ -392,26 +392,39 @@ function parseSignedWatchUrl(watchUrl, campaignId) {
|
|
|
392
392
|
}
|
|
393
393
|
try {
|
|
394
394
|
const url = new URL(watchUrl);
|
|
395
|
+
const directPath = `/campaign-builder/${campaignId}`;
|
|
396
|
+
const isDirectCampaignUrl = url.pathname === directPath &&
|
|
397
|
+
(url.searchParams.get("mode") === "claude" ||
|
|
398
|
+
url.searchParams.get("mode") === "codex" ||
|
|
399
|
+
url.searchParams.get("mode") === "watch");
|
|
400
|
+
if (isDirectCampaignUrl) {
|
|
401
|
+
return {
|
|
402
|
+
urlPresent: true,
|
|
403
|
+
direct: true,
|
|
404
|
+
redirectPath: `${url.pathname}?${url.searchParams.toString()}`.replace(/\?$/, ""),
|
|
405
|
+
warning: null,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
395
408
|
const redirect = url.searchParams.get("redirect");
|
|
396
409
|
const decodedRedirect = redirect ? decodeURIComponent(redirect) : null;
|
|
397
|
-
const
|
|
410
|
+
const isLegacySigned = url.pathname === "/auth/continue" &&
|
|
398
411
|
Boolean(url.searchParams.get("token")) &&
|
|
399
|
-
decodedRedirect
|
|
412
|
+
decodedRedirect?.startsWith(`${directPath}?mode=`) === true;
|
|
400
413
|
return {
|
|
401
414
|
urlPresent: true,
|
|
402
|
-
|
|
415
|
+
direct: false,
|
|
403
416
|
redirectPath: decodedRedirect,
|
|
404
|
-
warning:
|
|
405
|
-
?
|
|
406
|
-
: "Watch URL is not a
|
|
417
|
+
warning: isLegacySigned
|
|
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.",
|
|
407
420
|
};
|
|
408
421
|
}
|
|
409
422
|
catch {
|
|
410
423
|
return {
|
|
411
424
|
urlPresent: true,
|
|
412
|
-
|
|
425
|
+
direct: false,
|
|
413
426
|
redirectPath: null,
|
|
414
|
-
warning: "Watch URL is malformed; recover a fresh
|
|
427
|
+
warning: "Watch URL is malformed; recover a fresh safe direct campaign-builder watch link before browser handoff.",
|
|
415
428
|
};
|
|
416
429
|
}
|
|
417
430
|
}
|
|
@@ -554,7 +567,7 @@ export function computeCampaignNavigationStateFromCampaign(campaign, options) {
|
|
|
554
567
|
checkpoint: "send",
|
|
555
568
|
}
|
|
556
569
|
: describeVisibleStep(orientationStep);
|
|
557
|
-
const watchUrlState =
|
|
570
|
+
const watchUrlState = parseWatchUrl(campaign.watchUrl, campaign.id);
|
|
558
571
|
if (watchUrlState.warning) {
|
|
559
572
|
warnings.push(watchUrlState.warning);
|
|
560
573
|
}
|
|
@@ -579,7 +592,7 @@ export function computeCampaignNavigationStateFromCampaign(campaign, options) {
|
|
|
579
592
|
},
|
|
580
593
|
watchUrl: {
|
|
581
594
|
urlPresent: watchUrlState.urlPresent,
|
|
582
|
-
|
|
595
|
+
direct: watchUrlState.direct,
|
|
583
596
|
redirectPath: watchUrlState.redirectPath,
|
|
584
597
|
},
|
|
585
598
|
table: {
|
|
@@ -134,7 +134,7 @@ export declare function waitForCampaignTableReady(input: WaitForCampaignTableRea
|
|
|
134
134
|
};
|
|
135
135
|
watchUrl: {
|
|
136
136
|
urlPresent: boolean;
|
|
137
|
-
|
|
137
|
+
direct: boolean | undefined;
|
|
138
138
|
redirectPath: string | null;
|
|
139
139
|
};
|
|
140
140
|
table: {
|
|
@@ -183,7 +183,7 @@ export declare function waitForCampaignTableReady(input: WaitForCampaignTableRea
|
|
|
183
183
|
};
|
|
184
184
|
watchUrl: {
|
|
185
185
|
urlPresent: boolean;
|
|
186
|
-
|
|
186
|
+
direct: boolean | undefined;
|
|
187
187
|
redirectPath: string | null;
|
|
188
188
|
};
|
|
189
189
|
table: {
|
|
@@ -234,7 +234,7 @@ export declare function waitForCampaignTableReady(input: WaitForCampaignTableRea
|
|
|
234
234
|
};
|
|
235
235
|
watchUrl: {
|
|
236
236
|
urlPresent: boolean;
|
|
237
|
-
|
|
237
|
+
direct: boolean | undefined;
|
|
238
238
|
redirectPath: string | null;
|
|
239
239
|
};
|
|
240
240
|
table: {
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
function sanitizeWatchUrlValue(value) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
try {
|
|
3
|
+
const url = new URL(value);
|
|
4
|
+
if (url.pathname === "/auth/continue") {
|
|
5
|
+
const redirect = url.searchParams.get("redirect");
|
|
6
|
+
if (redirect) {
|
|
7
|
+
const direct = new URL(decodeURIComponent(redirect), url.origin);
|
|
8
|
+
const workspaceId = url.searchParams.get("workspaceId");
|
|
9
|
+
if (workspaceId && !direct.searchParams.has("workspaceId")) {
|
|
10
|
+
direct.searchParams.set("workspaceId", workspaceId);
|
|
11
|
+
}
|
|
12
|
+
return direct.toString();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
5
20
|
}
|
|
6
21
|
export function sanitizeWatchUrlsForMcpResult(value) {
|
|
7
22
|
if (!value || typeof value !== "object")
|
package/package.json
CHANGED
|
@@ -162,11 +162,11 @@ copy.
|
|
|
162
162
|
## Codex Watch Browser Handoff
|
|
163
163
|
|
|
164
164
|
When a campaign tool returns `watchUrl`, treat it as an active browser handoff,
|
|
165
|
-
not only a URL to print. A valid handoff link must be a
|
|
166
|
-
`/
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
165
|
+
not only a URL to print. A valid handoff link must be a safe direct
|
|
166
|
+
`/campaign-builder/{campaignId}?mode=claude` URL. `create_campaign.watchUrl`,
|
|
167
|
+
`create_campaign({ campaignId }).watchUrl`, and `get_campaign.watchUrl` are all
|
|
168
|
+
acceptable only when they return that direct campaign-builder shape, with
|
|
169
|
+
`workspaceId` used only as a safe routing hint when needed.
|
|
170
170
|
|
|
171
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
172
|
|
|
@@ -175,8 +175,8 @@ I’ll open the campaign view and keep it in sync as I build.
|
|
|
175
175
|
```
|
|
176
176
|
|
|
177
177
|
If browser control is unavailable, provide the watch link without discussing the
|
|
178
|
-
runtime limitation. In off-desktop Codex runs, make the
|
|
179
|
-
copy/open, then continue with explicit customer-facing campaign progress and
|
|
178
|
+
runtime limitation. In off-desktop Codex runs, make the direct watch link easy
|
|
179
|
+
to copy/open, then continue with explicit customer-facing campaign progress and
|
|
180
180
|
`get_campaign_navigation_state` orientation checks.
|
|
181
181
|
Use this fallback shape:
|
|
182
182
|
|
|
@@ -188,9 +188,9 @@ here as I build.
|
|
|
188
188
|
```
|
|
189
189
|
|
|
190
190
|
If opening the watch link lands on an auth, 404, permission, blank, or visible
|
|
191
|
-
error state, report only what is visible, recover a fresh
|
|
192
|
-
|
|
193
|
-
|
|
191
|
+
error state, report only what is visible, recover a fresh watch link once with
|
|
192
|
+
`create_campaign({ campaignId })` or `get_campaign`, and try that link. Do not
|
|
193
|
+
claim the browser was opened, inspected, or synchronized unless the visible
|
|
194
194
|
browser state was actually observed. Do not claim the browser was opened unless
|
|
195
195
|
it was opened and observed.
|
|
196
196
|
|
|
@@ -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,13 @@ 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. Do not promise time estimates in guide
|
|
190
|
+
copy.
|
|
184
191
|
- Every approval gate must include live campaign access after the readable inline
|
|
185
192
|
content. Show a short `Watch link:` line with the campaign link when a
|
|
186
193
|
campaign shell exists. In normal customer runs, do not show `Open artifact:`
|
|
@@ -401,7 +408,7 @@ Optional debug/UAT draft directory, disabled in normal customer runs:
|
|
|
401
408
|
`RevOps`, `outbound systems`, or `pipeline architecture` unless they are
|
|
402
409
|
translated into what the user can understand.
|
|
403
410
|
|
|
404
|
-
After rendering that brief, ask for brief approval before
|
|
411
|
+
After rendering that brief, ask for brief approval before Find Leads in
|
|
405
412
|
hosted net-new runs. The user-facing choice should be approve/revise language,
|
|
406
413
|
not "looks good".
|
|
407
414
|
Approval options should refer to what the user just read, e.g. `Approve this
|
|
@@ -422,16 +429,15 @@ brief`, `Revise target`, `Revise offer/proof`, and `Other / custom`.
|
|
|
422
429
|
|
|
423
430
|
The approval question can be based on the rendered brief in chat and the live
|
|
424
431
|
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.
|
|
432
|
+
Before Find Leads, call `update_campaign({ campaignId, campaignBrief,
|
|
433
|
+
currentStep: "pick-provider", watchNarration: { ... } })` after approval so
|
|
434
|
+
the watch link moves out of Plan while the main thread compares source paths.
|
|
428
435
|
|
|
429
436
|
- After the brief is approved, show the next progress line:
|
|
430
437
|
`Cool. Now I'm going to find people who are both a good fit and active enough
|
|
431
438
|
to be worth a LinkedIn test. I'll compare source paths by expected volume,
|
|
432
439
|
sampled ICP fit, activity/warmth signal, cleanup risk, and tradeoffs. This
|
|
433
|
-
|
|
434
|
-
anything goes live.`
|
|
440
|
+
will end with a source decision + sample before anything goes live.`
|
|
435
441
|
- In watch mode, do not leave the user sitting on only `pick-provider` while
|
|
436
442
|
source scouts run. Move the campaign to the likely primary source lane
|
|
437
443
|
(`signal-discovery`, `sales-nav`, or `prospeo`) before background source
|
|
@@ -447,9 +453,8 @@ message we should test.`
|
|
|
447
453
|
- During long post-intake work, show concise progress checkpoints before the
|
|
448
454
|
next expensive stage: source being checked, source switch/tradeoff if any,
|
|
449
455
|
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:
|
|
456
|
+
Each checkpoint should say what changed, what is being checked next, and why
|
|
457
|
+
that matters for the campaign. Do not invent remaining-time estimates. Example:
|
|
453
458
|
|
|
454
459
|
```text
|
|
455
460
|
This is taking a little longer than I expected, sorry. I’m being careful here
|
|
@@ -623,7 +628,7 @@ workstreams`, `in parallel`, or `background` unless parallel branches were
|
|
|
623
628
|
There are four customer-visible gates in the net-new hosted flow:
|
|
624
629
|
|
|
625
630
|
- `brief-review` asks whether to approve the rendered campaign brief before
|
|
626
|
-
|
|
631
|
+
Find Leads starts. Do not skip this by inferring approval from setup
|
|
627
632
|
answers.
|
|
628
633
|
- `lead-review` / source decision asks whether to approve the selected source
|
|
629
634
|
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": "
|
|
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",
|
|
@@ -755,7 +759,7 @@
|
|
|
755
759
|
"import the first 15-row review batch before fit/message work",
|
|
756
760
|
"selectedLeadListId stays the source list and workflowTableId is the campaign table"
|
|
757
761
|
],
|
|
758
|
-
"
|
|
762
|
+
"watchNarrationRule": "Do not include time estimates. Say what review-batch work is happening and what the user will approve next.",
|
|
759
763
|
"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
764
|
},
|
|
761
765
|
{
|
|
@@ -929,10 +933,12 @@
|
|
|
929
933
|
"tool": "update_campaign",
|
|
930
934
|
"requiredFields": [
|
|
931
935
|
"campaignId",
|
|
932
|
-
"currentStep"
|
|
936
|
+
"currentStep",
|
|
937
|
+
"watchNarration"
|
|
933
938
|
],
|
|
934
939
|
"requiredValues": {
|
|
935
|
-
"currentStep": "filter-choice"
|
|
940
|
+
"currentStep": "filter-choice",
|
|
941
|
+
"watchNarration.stage": "fit-message"
|
|
936
942
|
}
|
|
937
943
|
}
|
|
938
944
|
],
|
|
@@ -1489,7 +1495,8 @@
|
|
|
1489
1495
|
"message-review-decision.md"
|
|
1490
1496
|
],
|
|
1491
1497
|
"requiredValues": {
|
|
1492
|
-
"currentStep": "validate-sample"
|
|
1498
|
+
"currentStep": "validate-sample",
|
|
1499
|
+
"watchNarration.stage": "review-ready"
|
|
1493
1500
|
},
|
|
1494
1501
|
"when": "after_update_campaign_brief_succeeds",
|
|
1495
1502
|
"requiredBeforeCascade": true,
|
|
@@ -1846,7 +1853,7 @@
|
|
|
1846
1853
|
"Campaign created",
|
|
1847
1854
|
"approved brief",
|
|
1848
1855
|
"Open this to watch",
|
|
1849
|
-
"
|
|
1856
|
+
"Find Leads",
|
|
1850
1857
|
"rubric scoring",
|
|
1851
1858
|
"messaging populate live",
|
|
1852
1859
|
"Watch link:"
|
|
@@ -2118,10 +2125,12 @@
|
|
|
2118
2125
|
"tool": "update_campaign",
|
|
2119
2126
|
"requiredFields": [
|
|
2120
2127
|
"campaignId",
|
|
2121
|
-
"currentStep"
|
|
2128
|
+
"currentStep",
|
|
2129
|
+
"watchNarration"
|
|
2122
2130
|
],
|
|
2123
2131
|
"requiredValues": {
|
|
2124
|
-
"currentStep": "awaiting-user-greenlight"
|
|
2132
|
+
"currentStep": "awaiting-user-greenlight",
|
|
2133
|
+
"watchNarration.stage": "review-ready"
|
|
2125
2134
|
}
|
|
2126
2135
|
}
|
|
2127
2136
|
],
|
|
@@ -2188,7 +2197,7 @@
|
|
|
2188
2197
|
},
|
|
2189
2198
|
{
|
|
2190
2199
|
"action": "surface_sender_and_slack_handoff",
|
|
2191
|
-
"settingsUrlPattern": "/campaign-builder/{campaignId}/settings?mode=claude",
|
|
2200
|
+
"settingsUrlPattern": "/campaign-builder/{campaignId}/settings?mode={claude|codex}",
|
|
2192
2201
|
"requiredVisibleContent": [
|
|
2193
2202
|
"connect or select a LinkedIn sender",
|
|
2194
2203
|
"Slack reply review",
|
|
@@ -0,0 +1,141 @@
|
|
|
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
|
+
Review batch:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"stage": "review-batch",
|
|
94
|
+
"headline": "Preparing your review batch",
|
|
95
|
+
"visibleState": "This page shows the leads Codex selected for review.",
|
|
96
|
+
"agentIntent": "Codex is preparing the first campaign batch from those leads.",
|
|
97
|
+
"nextAction": "Fit + message"
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Fit + message:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"stage": "fit-message",
|
|
106
|
+
"headline": "Building fit and message",
|
|
107
|
+
"visibleState": "Codex is preparing the campaign's fit rules and first message sample.",
|
|
108
|
+
"agentIntent": "It is checking who should receive this campaign and what the first offer should say.",
|
|
109
|
+
"nextAction": "Review ready",
|
|
110
|
+
"workerStatuses": {
|
|
111
|
+
"leadFitBuilder": "running",
|
|
112
|
+
"messageDraftBuilder": "running"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Review ready:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"stage": "review-ready",
|
|
122
|
+
"headline": "Review fit and message",
|
|
123
|
+
"visibleState": "The fit rules and message sample are ready.",
|
|
124
|
+
"agentIntent": "Approve them or ask for changes in chat.",
|
|
125
|
+
"nextAction": "Validation",
|
|
126
|
+
"safety": "Codex will only continue after you approve."
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Blocked/recovering:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"stage": "review-batch",
|
|
135
|
+
"headline": "Fixing the review batch",
|
|
136
|
+
"visibleState": "The browser is not showing the campaign batch yet.",
|
|
137
|
+
"agentIntent": "Codex is reconnecting the campaign state before moving forward.",
|
|
138
|
+
"nextAction": "Fit + message",
|
|
139
|
+
"blockedReason": "Campaign batch is not ready yet."
|
|
140
|
+
}
|
|
141
|
+
```
|
|
@@ -11,12 +11,12 @@ gating, and handoff read campaign state first.
|
|
|
11
11
|
|
|
12
12
|
## Plumbing Reuse
|
|
13
13
|
|
|
14
|
-
`create_campaign` already returns a
|
|
15
|
-
(`CampaignDetail.watchUrl` in
|
|
16
|
-
NOT mint a new token, does NOT call
|
|
17
|
-
|
|
18
|
-
surface it verbatim. The link must
|
|
19
|
-
land on the campaign builder without
|
|
14
|
+
`create_campaign` already returns a safe direct campaign-builder `watchUrl` on
|
|
15
|
+
the response (`CampaignDetail.watchUrl` in
|
|
16
|
+
`mcp/sellable/src/tools/campaigns.ts`). V2 does NOT mint a new token, does NOT call
|
|
17
|
+
a different route, and does NOT construct the URL locally. Capture whatever
|
|
18
|
+
`watchUrl` the existing tool returns and surface it verbatim. The link must
|
|
19
|
+
land on the campaign builder without exposing token secrets in the MCP result.
|
|
20
20
|
|
|
21
21
|
## Shell-First Link
|
|
22
22
|
|
|
@@ -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
|
|
@@ -59,7 +59,7 @@ If shell creation fails or the response is missing `watchUrl`, stop and surface
|
|
|
59
59
|
the error. Do not continue into campaignless source scouting in the active
|
|
60
60
|
shell-first flow.
|
|
61
61
|
If a legacy atomic-mint response is missing `watchUrl`, it is not a silent skip.
|
|
62
|
-
Stop before `save_rubrics` and recover the missing
|
|
62
|
+
Stop before `save_rubrics` and recover the missing watch URL first.
|
|
63
63
|
|
|
64
64
|
## Step Orientation
|
|
65
65
|
|
|
@@ -81,19 +81,19 @@ Do not spam the link between internal tool calls.
|
|
|
81
81
|
On resume:
|
|
82
82
|
|
|
83
83
|
1. Load the `CampaignOffer` for the campaign being resumed.
|
|
84
|
-
2. Recover the
|
|
84
|
+
2. Recover the direct watch link through the existing resume path
|
|
85
85
|
(`create_campaign({ campaignId })` when available).
|
|
86
86
|
3. Inspect `currentStep` and print a short orientation for where the user will
|
|
87
87
|
land now.
|
|
88
88
|
4. Print the recovered `watchUrl`.
|
|
89
89
|
|
|
90
|
-
The re-surface must use the
|
|
90
|
+
The re-surface must use the `watchUrl` from the tool response, not a
|
|
91
91
|
cached or reconstructed URL.
|
|
92
92
|
|
|
93
93
|
## Hard Rules
|
|
94
94
|
|
|
95
95
|
- Never fabricate or reconstruct the URL locally.
|
|
96
|
-
- The watch link must
|
|
96
|
+
- The watch link must be a safe direct campaign-builder URL.
|
|
97
97
|
- Missing `watchUrl` is an error, not a silent skip.
|
|
98
98
|
- The first watch link must be shown only after the v1 brief is on the campaign.
|
|
99
99
|
- A watch link is not approval to import, attach sequence, or start.
|