@sellable/mcp 0.1.135 → 0.1.137

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.
@@ -1,7 +1,7 @@
1
1
  You are Lead Fit Builder for Sellable create-campaign-v2.
2
2
 
3
- Your job starts only after find-leads has produced `lead-review.md` and
4
- `lead-sample.json`, and the lead source has been approved or auto-confirmed.
3
+ Your job starts only after the lead source has been approved or auto-confirmed
4
+ and the bounded review batch exists in the campaign table.
5
5
  Work only on the lead filter branch. Do not source new leads, draft messages,
6
6
  import leads, create campaigns, or ask the user questions. Your only live
7
7
  campaign mutation is calling `save_rubrics` after the production rubrics are
@@ -9,41 +9,38 @@ ready.
9
9
 
10
10
  Required inputs:
11
11
 
12
- - `brief.md`
13
- - `lead-review.md`
14
- - `lead-sample.json`
15
- - campaign state from the parent thread
16
- - campaign table sample from the parent thread
12
+ - `campaignId`
13
+ - campaign revision or `campaignUpdatedAt`
14
+ - campaign brief content
15
+ - selected source decision and provider/list state
16
+ - `selectedLeadListId`
17
+ - `workflowTableId`
18
+ - imported review-batch rows, including row ids/hash when available
19
+ - filter choice
17
20
 
18
21
  Required first steps:
19
22
 
20
- 1. Read the three required inputs.
21
- 2. Load the filter-leads reference before writing artifacts:
23
+ 1. Verify the campaign/source/table ids from the parent thread match the live
24
+ campaign context.
25
+ 2. Load the filter-leads reference before designing rubrics:
22
26
  `get_subskill_asset({ subskillName: "create-campaign-v2", assetPath: "references/filter-leads.md" })`.
23
27
  3. Treat campaign state and the campaign table sample as the input of record.
24
- Disk files are context/debug aids, not durable state.
28
+ Do not require or hunt for local markdown/json artifacts.
25
29
 
26
30
  Owned outputs:
27
31
 
28
32
  - Durable campaign rubrics via `save_rubrics({ campaignOfferId, leadScoringRubrics })`
29
33
  when the filter is confirmed and production-shaped rubrics are safe to write.
30
34
  `save_rubrics` is the durable writer.
31
- - `lead-filter.md` debug artifact after the durable campaign write
32
- - `rubric.json` debug artifact after the durable campaign write
35
+ - concise filter/rubric summary returned to the parent thread
33
36
 
34
- Do not write or modify:
35
-
36
- - `message-validation.md`
37
- - `message-review.md`
38
- - `approval-packet.md`
39
- - `brief.md`
40
- - `lead-review.md`
41
- - `lead-sample.json`
37
+ Do not write or modify local markdown/json artifacts. Durable output is only
38
+ via `save_rubrics` plus the parent-thread summary.
42
39
 
43
40
  Process:
44
41
 
45
- 1. Preserve the approved source decision and the sample math from
46
- `lead-review.md`; do not re-run sourcing.
42
+ 1. Preserve the approved source decision and review-batch sample math supplied
43
+ by the parent; do not re-run sourcing.
47
44
  2. Turn the sample's good-fit and false-positive patterns into a strict but
48
45
  campaign-native filter.
49
46
  3. Include keep rules, exclude rules, sample false positives, pass-rate /
@@ -56,14 +53,11 @@ Process:
56
53
  6. If status is `confirmed`, call `save_rubrics` with 2-5 production-shaped
57
54
  active `leadScoringRubrics` before reporting success. If `save_rubrics`
58
55
  fails, stop and report the blocker; do not claim the filter is persisted.
59
- 7. Write `lead-filter.md` and `rubric.json` only as debug artifacts after
60
- campaign persistence succeeds.
61
56
 
62
57
  Return a concise final status with:
63
58
 
64
59
  - filter status: `confirmed`, `confirm-with-user`, or `revise-find-leads`
65
60
  - whether `save_rubrics` succeeded and how many active rubrics were persisted
66
- - artifacts written
67
61
  - strongest keep rules
68
62
  - strongest exclusion rules
69
63
  - expected pass-rate / yield impact
@@ -11,11 +11,14 @@ campaign state. The main thread owns approval and campaign writes.
11
11
  Use the live campaign inputs supplied by the parent thread:
12
12
 
13
13
  - `campaignId`
14
+ - campaign revision or `campaignUpdatedAt`
14
15
  - `campaignBrief` / campaign brief content model
15
16
  - selected source decision and provider state
16
17
  - `selectedLeadListId` or selected source list context
17
18
  - `workflowTableId`
18
- - imported review-batch rows from that selected list
19
+ - imported review-batch rows from that selected list, including row IDs and a
20
+ review-batch row hash when available
21
+ - filter basis at branch start: `pending`, `use-filters`, or `skip-filters`
19
22
  - any already-saved fit/rubric result summaries supplied by the parent
20
23
 
21
24
  Do not require or hunt for `brief.md`, `lead-review.md`, or `lead-sample.json`.
@@ -23,6 +26,13 @@ Those files are optional debug context only when the parent explicitly provides
23
26
  them. Never inspect the product database directly, never run `psql`, and never
24
27
  read stale local markdown files to reconstruct campaign state.
25
28
 
29
+ All live reads must come from scoped MCP/product tools by campaign and
30
+ workspace, such as `get_campaign`, `get_campaign_context`, and
31
+ `get_rows_minimal({ tableId: workflowTableId })`, or from equivalent parent
32
+ thread payloads. Reject the task as `blocked` if the campaign id, workspace,
33
+ `selectedLeadListId`, `workflowTableId`, or review-batch row ids do not match
34
+ the branch input.
35
+
26
36
  ## Required First Steps
27
37
 
28
38
  1. Load the full long-form generate-messages prompt:
@@ -45,11 +55,52 @@ Return the following to the parent thread:
45
55
  - one rendered good-fill sample for a plausible passing review-batch row
46
56
  - one omit/fallback sample when the row signal is not safe
47
57
  - pass/fail notes against the generate-messages quality gates
58
+ - compact runtime status: `ready`, `blocked`, `retry-needed`, or `stale`
59
+ - basis token containing campaign revision/updatedAt, brief hash,
60
+ `selectedLeadListId`, `workflowTableId`, review-batch row ids/hash, filter
61
+ choice, and rubric/filter basis when present
62
+ - output timestamp/hash and any retry/error detail
48
63
 
49
64
  Write `message-validation.md`, `message-prep.md`, or
50
65
  `message-candidate-drafts.md` only when the parent explicitly asks for debug
51
66
  artifacts. Normal live campaign runs can return the same content directly.
52
67
 
68
+ When reporting branch runtime proof, use this shape under
69
+ `watchNarration.workerDetails.messageDraftBuilder`:
70
+
71
+ - `statusSource`: `branch` or `parent-thread-fallback`
72
+ - `status`: `branch-running`, `fallback-active`, `spawn-failed`,
73
+ `fallback-superseded`, `branch-superseded`, `ready`, `blocked`,
74
+ `retry-needed`, or `stale`
75
+ - `runId` or `fallbackId`
76
+ - `startedAt` and `updatedAt`
77
+ - `basisToken` and `basis`
78
+ - optional `compactOutputRef`, `compactOutput`, and `error`
79
+
80
+ Do not tell the UI to show Message Draft Builder as running unless this proof
81
+ exists and points at the current non-empty bounded review batch.
82
+
83
+ ## Basis Changes And Rewrites
84
+
85
+ The first completed recommendation is the default message review candidate.
86
+ Do not automatically retry or regenerate only because Lead Fit Builder finished,
87
+ rubrics were saved, Filter Leads completed, enrichment cells populated, or more
88
+ row data became available after this branch started.
89
+
90
+ Treat later filter/enrichment data as optional rewrite context. If campaign id,
91
+ brief hash, selected source, `selectedLeadListId`, `workflowTableId`, and
92
+ review-batch row ids/hash still match, keep the initial recommendation usable
93
+ and report `status: ready` with `basisStatus: "usable_initial"` or
94
+ `"enriched_rewrite_available"`. The parent thread may offer the user a choice
95
+ to keep the initial draft or rewrite with enriched/filter data, but the rewrite
96
+ must be explicit user opt-in.
97
+
98
+ Retry or regenerate without asking only when the initial recommendation is
99
+ missing, failed, structurally invalid, unsafe, or mismatched on campaign id,
100
+ brief hash, selected source, `selectedLeadListId`, `workflowTableId`, or
101
+ review-batch rows. Filter/rubric/enrichment basis drift alone is not a stale
102
+ blocker.
103
+
53
104
  ## Hard Rules
54
105
 
55
106
  - Do not call product Generate Message cells. This worker drafts the template
@@ -158,22 +158,20 @@
158
158
  "displayName": "Lead Fit Builder",
159
159
  "target": "filter-leads",
160
160
  "inputs": [
161
- "brief.md",
162
- "lead-review.md",
163
- "lead-sample.json"
164
- ],
165
- "producesArtifacts": [
166
- "lead-filter.md"
167
- ],
168
- "optionalProducesArtifacts": [
169
- "rubric.json"
161
+ "campaignId",
162
+ "campaignBrief",
163
+ "source decision and selectedLeadList/source state",
164
+ "workflowTableId",
165
+ "imported review-batch rows"
170
166
  ],
167
+ "producesArtifacts": [],
168
+ "optionalProducesArtifacts": [],
171
169
  "ownership": "lead quality, false-positive patterns, keep/exclude rules, ability-to-pay checks, and production rubric translation only",
172
170
  "codex": {
173
171
  "description": "Lead Fit Builder for campaign-backed lead filtering and rubric persistence after source approval.",
174
172
  "model": "gpt-5.5",
175
173
  "modelReasoningEffort": "high",
176
- "sandboxMode": "workspace-write",
174
+ "sandboxMode": "read-only",
177
175
  "nicknameCandidates": [
178
176
  "Lead Fit Builder",
179
177
  "Fit Builder",
@@ -181,17 +179,12 @@
181
179
  ]
182
180
  },
183
181
  "claude": {
184
- "description": "Use proactively as Lead Fit Builder after lead source approval to persist campaign rubrics and write lead-filter debug artifacts from campaign state.",
182
+ "description": "Use proactively as Lead Fit Builder after lead source approval to persist campaign rubrics from campaign state.",
185
183
  "model": "inherit",
186
184
  "background": true,
187
185
  "maxTurns": 8,
188
186
  "color": "yellow",
189
187
  "tools": [
190
- "Read",
191
- "Write",
192
- "Edit",
193
- "Grep",
194
- "Glob",
195
188
  "mcp__sellable__get_subskill_prompt",
196
189
  "mcp__sellable__get_subskill_asset",
197
190
  "mcp__sellable__save_rubrics"
@@ -225,10 +218,10 @@
225
218
  ],
226
219
  "ownership": "proof inventory, token strategy, angle drafting, skeptical-prospect review, and selected winner only",
227
220
  "codex": {
228
- "description": "Message Draft Builder for campaign-backed template proposals and sample messages after source approval.",
221
+ "description": "Message Draft Builder for campaign-backed template proposals after confirm_lead_list imports a non-empty bounded review batch.",
229
222
  "model": "gpt-5.5",
230
223
  "modelReasoningEffort": "high",
231
- "sandboxMode": "workspace-write",
224
+ "sandboxMode": "read-only",
232
225
  "nicknameCandidates": [
233
226
  "Message Draft Builder",
234
227
  "Draft Builder",
@@ -236,19 +229,17 @@
236
229
  ]
237
230
  },
238
231
  "claude": {
239
- "description": "Use proactively as Message Draft Builder after lead source approval to propose an approved-template candidate and sample message from campaign state.",
232
+ "description": "Use proactively as Message Draft Builder after confirm_lead_list imports a non-empty bounded review batch; load the full generate-messages prompt and draft only from scoped campaign/tool state.",
240
233
  "model": "inherit",
241
234
  "background": true,
242
235
  "maxTurns": 10,
243
236
  "color": "magenta",
244
237
  "tools": [
245
238
  "Read",
246
- "Write",
247
- "Edit",
248
- "Grep",
249
- "Glob",
250
239
  "mcp__sellable__get_subskill_prompt",
251
- "mcp__sellable__get_subskill_asset"
240
+ "mcp__sellable__get_campaign",
241
+ "mcp__sellable__get_campaign_context",
242
+ "mcp__sellable__get_rows_minimal"
252
243
  ]
253
244
  }
254
245
  }
@@ -8,9 +8,10 @@ Required first step:
8
8
  draft `campaignOfferId`, call `get_provider_prompt({ provider:
9
9
  "signal-discovery", campaignOfferId, confirmed: true })` and include that same
10
10
  `campaignOfferId` plus `currentStep: "signal-discovery"` in `search_signals`
11
- so the user can watch source work in the campaign UI. Treat that as a
12
- campaign-attached persisted search; do not run a post-mint search without the
13
- campaign ID. If no campaign ID is supplied, run campaignless preview mode.
11
+ so the owning search route can show the source lane with current find-leads
12
+ narration and user options. Treat that as a campaign-attached persisted search;
13
+ do not run a post-mint search without the campaign ID. If no campaign
14
+ ID is supplied, run campaignless preview mode.
14
15
 
15
16
  Use the inherited Sellable MCP tools when available:
16
17
 
@@ -44,6 +45,8 @@ sales Claude`; do not treat broad anchor-only lanes like `Claude Code`, `MCP`,
44
45
  campaignOfferId, selectionMode: "replace", selections, headlineICPCriteria,
45
46
  currentStep: "signal-discovery" })` before sampling so the watched Signal
46
47
  Discovery table shows the promoted posts and the exact posts being tested.
48
+ Do not move the campaign to `confirm-lead-list`; `import_leads` owns that
49
+ visible transition after source approval.
47
50
  6. Fetch or sample engagers for promoted posts and score rough ICP fit from
48
51
  visible headline/display-name cues only. Do not enrich people during
49
52
  viability estimation.
@@ -8,10 +8,12 @@ Required first step:
8
8
  draft `campaignOfferId`, call `get_provider_prompt({ provider: "prospeo",
9
9
  campaignOfferId, confirmed: true })` and include that same `campaignOfferId`
10
10
  plus `currentStep: "prospeo"` in `search_prospeo` so the user can watch source
11
- work in the campaign UI. If no campaign ID is supplied, run campaignless
12
- preview mode. Treat post-mint
11
+ work in the campaign UI with source-lane narration owned by the search route.
12
+ If no campaign ID is supplied, run campaignless preview mode. Treat post-mint
13
13
  searches with `campaignOfferId` as campaign-attached persisted search tabs;
14
14
  do not run a live campaign search without the campaign ID.
15
+ Do not move the campaign to `confirm-lead-list`; `import_leads` owns that
16
+ visible transition after source approval.
15
17
 
16
18
  Use the inherited Sellable MCP tools when available:
17
19
 
@@ -8,10 +8,12 @@ Required first step:
8
8
  draft `campaignOfferId`, call `get_provider_prompt({ provider: "sales-nav",
9
9
  campaignOfferId, confirmed: true })` and include that same `campaignOfferId` in
10
10
  `search_sales_nav` with `currentStep: "sales-nav"` so the user can watch
11
- source work in the campaign UI. If no campaign ID is supplied, run
12
- campaignless preview mode. Treat post-mint
11
+ source work in the campaign UI with source-lane narration owned by the search
12
+ route. If no campaign ID is supplied, run campaignless preview mode. Treat post-mint
13
13
  searches with `campaignOfferId` as campaign-attached persisted search tabs;
14
14
  do not run a live campaign search without the campaign ID.
15
+ Do not move the campaign to `confirm-lead-list`; `import_leads` owns that
16
+ visible transition after source approval.
15
17
 
16
18
  Use the inherited Sellable MCP tools when available:
17
19
 
@@ -270,7 +270,7 @@ export async function bootstrapCreateCampaign(input = {}) {
270
270
  ? resumeDetected
271
271
  ? `Bootstrap complete.${workspaceNotice} Resume from campaign state and navigation diagnostics first; treat local draft artifacts as debug-only evidence. Then load ${createCampaignSubskill?.name ?? "create-campaign"} instructions with get_subskill_prompt({ subskillName: "${createCampaignSubskill?.name ?? "create-campaign"}" }); if the response has hasMore=true, continue with nextOffset until hasMore=false.`
272
272
  : flowVersion === "v2"
273
- ? `Bootstrap complete.${workspaceNotice} Load create-campaign-v2 instructions with get_subskill_prompt({ subskillName: "create-campaign-v2" }); if the response has hasMore=true, continue with nextOffset until hasMore=false. Preserve the pre-intake sequence: confirm auth/workspace status, identity setup, sender discovery/listing, and lightweight profile/company lookup. Then run the short setup/intake, write the campaign brief, call create_campaign once to mint the watchable shell, surface the returned watch link, and hand off to lead finding.`
273
+ ? `Bootstrap complete.${workspaceNotice} Load the compact create-campaign-v2 entry prompt once with get_subskill_prompt({ subskillName: "create-campaign-v2" }); load flow/reference assets lazily only when that stage needs them. Preserve the pre-intake sequence: confirm auth/workspace status, resolve campaign identity, and run lightweight profile/company lookup. Do not call list_senders or sender discovery during setup; sender availability belongs only to Settings after message approval. Then run the short setup/intake, write the campaign brief, call create_campaign once to mint the watchable shell, surface the returned watch link, and hand off to lead finding.`
274
274
  : `Bootstrap complete.${workspaceNotice} Load ${createCampaignSubskill?.name ?? "create-campaign"} instructions with get_subskill_prompt({ subskillName: "${createCampaignSubskill?.name ?? "create-campaign"}" }); if the response has hasMore=true, continue with nextOffset until hasMore=false. Follow that flow before calling create_campaign.`
275
275
  : "Bootstrap incomplete. Resolve blockingErrors and rerun bootstrap_create_campaign before provider/search/import tools.";
276
276
  // Strip prompt body from createCampaignSubskill — it's loaded via the host
@@ -843,17 +843,6 @@ export async function updateCampaign(campaignId, input) {
843
843
  }
844
844
  }
845
845
  }
846
- // Hotfix: when rubrics are saved with enableICPFilters, ensure the UI step
847
- // is set to "filter-rules" so the frontend can render the filter state.
848
- // Without this, the MCP caller can save rubrics + enableICPFilters on the
849
- // backend while the UI currentStep points somewhere else (e.g. "messages"),
850
- // causing the UI to be out of sync with the backend filter state.
851
- if (updates.enableICPFilters === true &&
852
- Array.isArray(updates.rubric) &&
853
- updates.rubric.length > 0 &&
854
- !updates.currentStep) {
855
- updates.currentStep = "filter-rules";
856
- }
857
846
  let result = null;
858
847
  let senderRouteResult = null;
859
848
  const shouldPreserveInteractionModePut = typeof interactionMode === "string" &&
@@ -2540,6 +2540,20 @@ export declare function confirmLeadList(input: ConfirmLeadListInput): Promise<{
2540
2540
  remainingRowCount?: number | null;
2541
2541
  rowCount?: number | null;
2542
2542
  };
2543
+ messageDraftBuilder: {
2544
+ firstAllowedStartPoint: string;
2545
+ startAllowed: boolean;
2546
+ requiredBeforeRunningCopy: string;
2547
+ requiredLiveInputs: {
2548
+ campaignOfferId: string;
2549
+ selectedLeadListId: string;
2550
+ workflowTableId: string | null;
2551
+ reviewBatchRowIds: string[];
2552
+ reviewBatchRowCount: number;
2553
+ };
2554
+ branchBasisFields: string[];
2555
+ promptRequired: string;
2556
+ };
2543
2557
  boundedReviewBatch: {
2544
2558
  requestedTargetLeadCount: number;
2545
2559
  importedRowCount: number;
@@ -429,6 +429,81 @@ function summarizeSignalSearchResponse(response) {
429
429
  ],
430
430
  };
431
431
  }
432
+ function buildSourceImportWatchNarration({ provider, selectedPostCount, estimatedEngagers, targetLeadCount, }) {
433
+ const providerLabel = provider === "signal-discovery"
434
+ ? "Signal Leads"
435
+ : provider === "sales-nav"
436
+ ? "Sales Nav"
437
+ : provider === "prospeo"
438
+ ? "Prospeo"
439
+ : "Apollo";
440
+ const sourceDetail = provider === "signal-discovery"
441
+ ? `${selectedPostCount ?? "selected"} approved post${selectedPostCount === 1 ? "" : "s"}${typeof estimatedEngagers === "number"
442
+ ? ` with about ${estimatedEngagers.toLocaleString("en-US")} engagers`
443
+ : ""}`
444
+ : `the approved ${providerLabel} source`;
445
+ const targetDetail = typeof targetLeadCount === "number"
446
+ ? ` Targeting ${targetLeadCount.toLocaleString("en-US")} source leads before the bounded review batch is cloned.`
447
+ : "";
448
+ return {
449
+ stage: "review-batch",
450
+ headline: provider === "signal-discovery"
451
+ ? "Scraping source leads from posts"
452
+ : "Importing source leads",
453
+ visibleState: `The browser is showing ${providerLabel} import progress for ${sourceDetail}.${targetDetail}`,
454
+ agentIntent: "Codex is materializing the approved source into a lead list before cloning only the bounded review batch into the campaign.",
455
+ nextAction: "Wait for source leads, then import the 15-row review batch",
456
+ safety: "No enrichment, sequence, or sending starts during source import.",
457
+ };
458
+ }
459
+ function buildSourceImportRecoveryWatchNarration(args) {
460
+ const providerLabel = args.provider === "signal-discovery"
461
+ ? "Signal Leads"
462
+ : args.provider === "sales-nav"
463
+ ? "Sales Nav"
464
+ : args.provider === "prospeo"
465
+ ? "Prospeo"
466
+ : "source";
467
+ const reasonCopy = args.reason === "failed"
468
+ ? `${providerLabel} import failed before review rows were ready.`
469
+ : args.reason === "zero"
470
+ ? `${providerLabel} import finished without usable review rows.`
471
+ : args.reason === "timeout"
472
+ ? `${providerLabel} import has not produced review rows within the wait window.`
473
+ : `${providerLabel} import is still materializing source rows.`;
474
+ return {
475
+ stage: "review-batch",
476
+ headline: args.reason === "failed" || args.reason === "zero"
477
+ ? "Source import needs attention"
478
+ : "Source import still running",
479
+ visibleState: `${reasonCopy} The browser should stay on the lead import/review-batch screen.`,
480
+ agentIntent: "Codex is holding the campaign before filter-choice until a non-empty bounded review batch exists.",
481
+ nextAction: args.reason === "failed" || args.reason === "zero"
482
+ ? "Retry the import or change the approved source"
483
+ : "Wait again, retry readiness, or change the source",
484
+ safety: "No enrichment, filtering, message cells, sequence, or sending has started.",
485
+ };
486
+ }
487
+ function buildFilterChoiceWatchNarration({ sourceLeadCount, reviewRowCount, }) {
488
+ const sourceCopy = typeof sourceLeadCount === "number" && sourceLeadCount > 0
489
+ ? ` from ${sourceLeadCount.toLocaleString("en-US")} source candidate${sourceLeadCount === 1 ? "" : "s"}`
490
+ : "";
491
+ const reviewCopy = typeof reviewRowCount === "number" && reviewRowCount > 0
492
+ ? `${reviewRowCount.toLocaleString("en-US")} review lead${reviewRowCount === 1 ? " is" : "s are"} in the campaign table${sourceCopy}.`
493
+ : `The bounded review batch is in the campaign table${sourceCopy}.`;
494
+ return {
495
+ stage: "fit-message",
496
+ headline: "I recommend adding filters",
497
+ visibleState: `${reviewCopy} The browser is showing the filter-choice screen and sample rows.`,
498
+ agentIntent: "Codex is pausing on this sample because a mixed review list should be filtered before message review. Skip filters only if the visible rows already look clean.",
499
+ nextAction: "Choose filters or skip",
500
+ safety: "No enrichment, filtering, Generate Message cells, sequence, or sending starts from this choice.",
501
+ workerStatuses: {
502
+ leadFitBuilder: "idle",
503
+ messageDraftBuilder: "idle",
504
+ },
505
+ };
506
+ }
432
507
  function normalizeImportProvider(provider) {
433
508
  if (provider === "apollo-ai" || provider === "apollo")
434
509
  return "apollo";
@@ -960,7 +1035,7 @@ export const leadToolDefinitions = [
960
1035
  },
961
1036
  {
962
1037
  name: "import_leads",
963
- description: "Create/select a lead list and start the provider import job. Requires provider prompt preflight via get_provider_prompt for the active provider. Sets selectedLeadListId to the lead list so the user can review it. Does NOT import into the campaign table. After the user confirms the list, call confirm_lead_list.",
1038
+ description: "Create/select a lead list and start the provider import job. Requires provider prompt preflight via get_provider_prompt for the active provider. On success, this tool owns moving the watched campaign to confirm-lead-list with import/progress narration after a leadListId/jobId exists; do not call update_campaign to fix that step. After the user confirms the list, call confirm_lead_list.",
964
1039
  inputSchema: {
965
1040
  type: "object",
966
1041
  properties: {
@@ -1053,7 +1128,7 @@ export const leadToolDefinitions = [
1053
1128
  },
1054
1129
  {
1055
1130
  name: "confirm_lead_list",
1056
- description: "After the user confirms the lead list looks good, import it into the campaign table (clone). selectedLeadListId remains the source lead list; workflowTableId is the campaign table. Recommended post-confirm order: update_campaign(currentStep='filter-choice') -> wait_for_campaign_table_ready -> get_rows_minimal.",
1131
+ description: "After the user confirms the lead list looks good, import the bounded review batch into the campaign table. This tool owns moving the watched campaign to filter-choice with sample-assessment narration only after non-empty review rows exist; do not call update_campaign to fix filter-choice afterward. selectedLeadListId remains the source lead list; workflowTableId is the campaign table.",
1057
1132
  inputSchema: {
1058
1133
  type: "object",
1059
1134
  properties: {
@@ -1067,7 +1142,7 @@ export const leadToolDefinitions = [
1067
1142
  },
1068
1143
  currentStep: {
1069
1144
  type: ["string", "null"],
1070
- description: 'Headless workflow step ID. If omitted, defaults to "confirm-lead-list". Pass null to skip auto-advance.',
1145
+ description: 'Headless workflow step ID. If omitted, defaults to "filter-choice" after review rows exist. Pass null to skip auto-advance.',
1071
1146
  },
1072
1147
  campaignName: {
1073
1148
  type: "string",
@@ -2061,6 +2136,17 @@ export async function importLeads(input) {
2061
2136
  await api.put(`/api/v2/campaign-offers/${campaignOfferId}`, {
2062
2137
  selectedLeadListId: result.tableId,
2063
2138
  ...(shouldSetCurrentStep ? { currentStep: effectiveCurrentStep } : {}),
2139
+ ...(shouldSetCurrentStep
2140
+ ? {
2141
+ watchNarration: buildSourceImportWatchNarration({
2142
+ provider: "signal-discovery",
2143
+ selectedPostCount: postsToScrape.length,
2144
+ estimatedEngagers: result.estimatedEngagers,
2145
+ targetLeadCount: normalizePositiveInteger(targetEngagerCount) ??
2146
+ result.estimatedEngagers,
2147
+ }),
2148
+ }
2149
+ : {}),
2064
2150
  });
2065
2151
  return {
2066
2152
  provider: "signal-discovery",
@@ -2075,7 +2161,7 @@ export async function importLeads(input) {
2075
2161
  targetLeadCount: cappedTargetLeadCount ?? null,
2076
2162
  message: `Started scraping ${postsToScrape.length} posts (~${result.estimatedEngagers} engagers). Leads will appear as scraping completes.${importSelection.limited
2077
2163
  ? ` Limited from ${uniqueSelectedPosts.length} selected posts by the approved source-capacity scrape plan.`
2078
- : ""}`,
2164
+ : ""} The watched campaign has been moved to confirm-lead-list with import progress copy; do not call update_campaign to fix that step.`,
2079
2165
  };
2080
2166
  }
2081
2167
  // === SALES NAV / PROSPEO FLOW ===
@@ -2172,6 +2258,14 @@ export async function importLeads(input) {
2172
2258
  await api.put(`/api/v2/campaign-offers/${campaignOfferId}`, {
2173
2259
  selectedLeadListId: leadListId,
2174
2260
  ...(shouldSetCurrentStep ? { currentStep: effectiveCurrentStep } : {}),
2261
+ ...(shouldSetCurrentStep
2262
+ ? {
2263
+ watchNarration: buildSourceImportWatchNarration({
2264
+ provider,
2265
+ targetLeadCount: cappedTargetLeadCount ?? null,
2266
+ }),
2267
+ }
2268
+ : {}),
2175
2269
  });
2176
2270
  return {
2177
2271
  provider,
@@ -2180,7 +2274,7 @@ export async function importLeads(input) {
2180
2274
  jobResult,
2181
2275
  jobId,
2182
2276
  targetLeadCount: cappedTargetLeadCount ?? null,
2183
- message: "Import started. Review the lead list as it fills; once it looks good, confirm to import into the campaign table.",
2277
+ message: "Import started and the watched campaign has been moved to confirm-lead-list with import progress copy. Review the lead list as it fills; once it looks good, call confirm_lead_list. Do not call update_campaign to fix the import step.",
2184
2278
  };
2185
2279
  }
2186
2280
  export async function cancelLeadImport(input) {
@@ -2332,12 +2426,24 @@ export async function confirmLeadList(input) {
2332
2426
  }
2333
2427
  if (!readiness.ready) {
2334
2428
  if (readiness.reason === "missing_job_id") {
2335
- throw new Error("Import job ID is missing. Keep the campaign at confirm-lead-list; provide the jobId, retry readiness, cancel the import, or re-run-source before confirming.");
2429
+ const recoveryNarration = buildSourceImportRecoveryWatchNarration({
2430
+ reason: "pending",
2431
+ provider: resolvedProvider,
2432
+ });
2433
+ throw new Error(`${recoveryNarration.headline}: Import job ID is missing. Keep the campaign at confirm-lead-list; provide the jobId, retry readiness, cancel the import, or re-run-source before confirming. ${recoveryNarration.safety}`);
2336
2434
  }
2337
2435
  if (readiness.reason === "import_failed") {
2338
- throw new Error("Import failed. Keep the campaign at confirm-lead-list; retry the provider import, cancel it, or re-run-source before confirming.");
2436
+ const recoveryNarration = buildSourceImportRecoveryWatchNarration({
2437
+ reason: "failed",
2438
+ provider: resolvedProvider,
2439
+ });
2440
+ throw new Error(`${recoveryNarration.headline}: Import failed. Keep the campaign at confirm-lead-list; retry the provider import, cancel it, or re-run-source before confirming. ${recoveryNarration.safety}`);
2339
2441
  }
2340
- 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.");
2442
+ const recoveryNarration = buildSourceImportRecoveryWatchNarration({
2443
+ reason: "timeout",
2444
+ provider: resolvedProvider,
2445
+ });
2446
+ throw new Error(`${recoveryNarration.headline}: 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. ${recoveryNarration.safety}`);
2341
2447
  }
2342
2448
  const isTerminalAccessError = (error) => error instanceof SellableApiError && [401, 403, 404].includes(error.status);
2343
2449
  const formatTerminalAccessError = (error) => {
@@ -2353,7 +2459,7 @@ export async function confirmLeadList(input) {
2353
2459
  campaignName,
2354
2460
  keepInSync,
2355
2461
  ...(typeof targetLeadCount === "number" ? { targetLeadCount } : {}),
2356
- ...(shouldSetCurrentStep ? { currentStep: effectiveCurrentStep } : {}),
2462
+ currentStep: null,
2357
2463
  })
2358
2464
  .catch((error) => {
2359
2465
  if (isTerminalAccessError(error)) {
@@ -2375,9 +2481,15 @@ export async function confirmLeadList(input) {
2375
2481
  ? importedRowIds.length
2376
2482
  : typeof importResult.rowCount === "number"
2377
2483
  ? importResult.rowCount
2378
- : typeof importResult.leadsImported === "number"
2379
- ? importResult.leadsImported
2380
- : 0;
2484
+ : typeof importResult.leadsImported === "number" &&
2485
+ typeof importResult.leadsSkipped === "number"
2486
+ ? importResult.leadsImported + importResult.leadsSkipped
2487
+ : typeof importResult.leadsImported === "number"
2488
+ ? importResult.leadsImported
2489
+ : 0;
2490
+ const keptReviewRowCount = requestedTargetLeadCount !== null
2491
+ ? Math.min(importedRowCount, requestedTargetLeadCount)
2492
+ : importedRowCount;
2381
2493
  const remainingRowCount = typeof importResult.remainingRowCount === "number"
2382
2494
  ? importResult.remainingRowCount
2383
2495
  : 0;
@@ -2392,7 +2504,11 @@ export async function confirmLeadList(input) {
2392
2504
  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.");
2393
2505
  }
2394
2506
  if (importedRowCount <= 0) {
2395
- throw new Error("No usable review rows were kept for the campaign table. Tighten or change the source before continuing.");
2507
+ const recoveryNarration = buildSourceImportRecoveryWatchNarration({
2508
+ reason: "zero",
2509
+ provider: resolvedProvider,
2510
+ });
2511
+ throw new Error(`${recoveryNarration.headline}: No usable review rows were kept for the campaign table. Retry the import or change the approved source before continuing. ${recoveryNarration.safety}`);
2396
2512
  }
2397
2513
  if (campaignTableId && overflowRowIds.length > 0) {
2398
2514
  const deleteBatchSize = 25;
@@ -2413,12 +2529,41 @@ export async function confirmLeadList(input) {
2413
2529
  if (shouldSetCurrentStep) {
2414
2530
  await api.put(`/api/v2/campaign-offers/${campaignOfferId}`, {
2415
2531
  currentStep: effectiveCurrentStep,
2532
+ ...(effectiveCurrentStep === "filter-choice"
2533
+ ? {
2534
+ watchNarration: buildFilterChoiceWatchNarration({
2535
+ sourceLeadCount: leadListRowCount,
2536
+ reviewRowCount: keptReviewRowCount,
2537
+ }),
2538
+ }
2539
+ : {}),
2416
2540
  });
2417
2541
  }
2418
2542
  return {
2419
2543
  sourceLeadListId: resolvedLeadListId,
2420
2544
  campaignTableId: campaignTableId ?? null,
2421
2545
  importResult,
2546
+ messageDraftBuilder: {
2547
+ firstAllowedStartPoint: "confirm_lead_list",
2548
+ startAllowed: true,
2549
+ requiredBeforeRunningCopy: "Persist watchNarration.workerDetails.messageDraftBuilder with runId or fallbackId, statusSource, status, startedAt, updatedAt, and current basis before showing Message Draft Builder as In Progress.",
2550
+ requiredLiveInputs: {
2551
+ campaignOfferId,
2552
+ selectedLeadListId: resolvedLeadListId,
2553
+ workflowTableId: campaignTableId ?? null,
2554
+ reviewBatchRowIds: importedRowIds.slice(0, keptReviewRowCount),
2555
+ reviewBatchRowCount: keptReviewRowCount,
2556
+ },
2557
+ branchBasisFields: [
2558
+ "campaign revision or updatedAt",
2559
+ "brief hash",
2560
+ "selectedLeadListId",
2561
+ "workflowTableId",
2562
+ "bounded review-batch row ids/hash",
2563
+ "filter choice at branch start",
2564
+ ],
2565
+ promptRequired: 'Load get_subskill_prompt({ subskillName: "generate-messages", offset, limit }) until hasMore=false before drafting.',
2566
+ },
2422
2567
  boundedReviewBatch: requestedTargetLeadCount !== null
2423
2568
  ? {
2424
2569
  requestedTargetLeadCount,
@@ -2431,8 +2576,8 @@ export async function confirmLeadList(input) {
2431
2576
  : undefined,
2432
2577
  message: requestedTargetLeadCount !== null &&
2433
2578
  leadListRowCount > requestedTargetLeadCount
2434
- ? `I found ${leadListRowCount} source candidates and imported the first ${Math.min(importedRowCount, requestedTargetLeadCount)} into the campaign review table.`
2435
- : "Lead list imported into the campaign review table. Next: sample rows with get_rows_minimal or continue into filter work.",
2579
+ ? `I found ${leadListRowCount} source candidates and imported the first ${Math.min(importedRowCount, requestedTargetLeadCount)} into the campaign review table. The watched campaign is now on filter-choice with sample assessment copy; announce Message Draft Builder background start only if that branch has actually started.`
2580
+ : "Lead list imported into the campaign review table. The watched campaign is now on filter-choice with sample assessment copy; announce Message Draft Builder background start only if that branch has actually started.",
2436
2581
  };
2437
2582
  }
2438
2583
  export function getProviderPrompt(input) {