@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.
@@ -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;
@@ -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 lead sourcing steps (step-by-step | autonomous | ask-when-needed).",
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, `/campaign-builder/${campaignId}?mode=claude`);
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, `/campaign-builder/${existing.id}?mode=claude`);
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, `/campaign-builder/${result.id}?mode=claude`);
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,
@@ -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 {
@@ -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 and remaining target rows are resolved as failed/not processing, the tool returns ready:true with partial:true instead of forcing a timeout.",
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 && noActiveProcessing) {
689
+ if (minPassFloorMet) {
690
690
  const percent = completed > 0 ? Math.round((passed / completed) * 100) : 0;
691
- const reason = unresolvedRowsResolvedAsFailures
692
- ? "min_passed_count_met_with_resolved_failures"
693
- : "min_passed_count_met_no_active_processing";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.111",
3
+ "version": "0.1.113",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -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", "lead sourcing", and "approval" instead of
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 lead sourcing in
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 lead sourcing, call `update_campaign({ campaignId, campaignBrief,
426
- currentStep: "pick-provider" })` after approval so the watch link moves out of
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
- usually takes ~3-5 min, and I'll show you the source decision + sample before
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 include a rough remaining time when useful. If a step
451
- exceeds its estimate by roughly a minute or more, acknowledge it lightly once
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
- lead sourcing starts. Do not skip this by inferring approval from setup
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 and live campaign currentStep"
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 lead sourcing reads CampaignOffer.campaignBrief, not brief.md.",
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 Contacts before the main thread starts comparing source paths. Do not mention MCP or local artifacts."
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
- "timeEstimate": "~3-5 min"
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
- "timeEstimate": "~2-3 min",
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": "wait_for_post_lead_artifacts",
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
- "reconciliationRule": "Before entering message-review, verify lead-filter.md and message-validation.md both came from the same brief.md, lead-review.md, and lead-sample.json. lead-filter.md gates the sample rows; lead-sample.json remains the message sample source."
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": false,
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
- "lead sourcing",
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": "wait_for_bounded_review_batch_cascade_to_drain"
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": "sample.minProjectedPass (3)",
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 a 15-row batch with resolved failures can advance when the 3-pass floor is already met.",
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 when passed rows are below minProjectedPass or generated messages are incomplete.",
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 passed rows are below `sample.minProjectedPass` or generated messages
78
- are missing for passing rows, stop before Settings with:
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 projectedPass >= minProjectedPass:
93
- proceed to Step 15 (auto-execute-messaging) with this review batch
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 the pass count is below
170
- the configured floor or message generation has not covered the passing rows,
171
- stop at `Status: sample-needs-revision` before Settings. Show the completed /
172
- passed / pending counts and ask whether to revise source, revise filter/rubric,
173
- or wait once only if active processing is still visible.
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 lead sourcing, rubric scoring, and messaging populate live.
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 for it to drain, OBSERVE the results,
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 drains here
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
- compute projectedPass
49
- if OK: update_campaign(currentStep=auto-execute-messaging)
50
- else: diagnose brief-vs-list; if brief: update_campaign_brief + re-queue + wait
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 on completion.)
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-drain
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 for the
279
- table cascade to drain, then reads the resulting ICP scores and Passes Rubric
280
- values. It does NOT call `check_rubric`, `bulk_enrich_with_prospeo`, or any
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: minProjectedPass, includeRows: false })
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 partial === true and reason === "min_passed_count_met_with_resolved_failures":
294
- use the partial passRate/stats as a valid sample diagnostic
295
- continue if projectedPass >= minProjectedPass and generated messages are present for the passing rows
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 active processing is visible:
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