@sellable/mcp 0.1.80 → 0.1.82

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.
Files changed (36) hide show
  1. package/agents/post-find-leads-filter-scout.md +19 -8
  2. package/agents/post-find-leads-message-scout.md +21 -6
  3. package/agents/registry.json +33 -18
  4. package/agents/source-scout-linkedin-engagement.md +10 -2
  5. package/agents/source-scout-prospeo-contact.md +9 -2
  6. package/agents/source-scout-sales-nav.md +10 -2
  7. package/dist/server.js +7 -1
  8. package/dist/tools/bootstrap.js +2 -2
  9. package/dist/tools/campaigns.d.ts +12 -0
  10. package/dist/tools/campaigns.js +40 -5
  11. package/dist/tools/context.d.ts +10 -4
  12. package/dist/tools/context.js +8 -4
  13. package/dist/tools/leads.d.ts +6 -3
  14. package/dist/tools/leads.js +39 -43
  15. package/dist/tools/navigation.d.ts +28 -1
  16. package/dist/tools/navigation.js +220 -10
  17. package/dist/tools/prompts.js +1 -1
  18. package/dist/tools/provider-preflight.js +2 -3
  19. package/dist/tools/readiness.d.ts +31 -6
  20. package/dist/tools/readiness.js +6 -1
  21. package/dist/tools/rubrics.d.ts +2 -0
  22. package/dist/tools/rubrics.js +2 -0
  23. package/package.json +1 -1
  24. package/skills/create-campaign/SKILL.md +24 -12
  25. package/skills/create-campaign-v2/SKILL.md +166 -114
  26. package/skills/create-campaign-v2/SOUL.md +19 -11
  27. package/skills/create-campaign-v2/core/auto-execute.yaml +14 -13
  28. package/skills/create-campaign-v2/core/flow.v2.json +964 -310
  29. package/skills/create-campaign-v2/references/approval-gate-framing.md +64 -32
  30. package/skills/create-campaign-v2/references/filter-leads.md +8 -0
  31. package/skills/create-campaign-v2/references/final-handoff-contract.md +55 -20
  32. package/skills/create-campaign-v2/references/lead-validation-preview.md +30 -17
  33. package/skills/create-campaign-v2/references/step-13-import-leads.md +62 -17
  34. package/skills/create-campaign-v2/references/validation-criteria.md +10 -5
  35. package/skills/create-campaign-v2/references/watch-link-handoff.md +72 -72
  36. package/skills/create-campaign-v2-tail/SKILL.md +123 -92
@@ -1,28 +1,35 @@
1
- You are the Post-Lead Filter Scout for Sellable create-campaign-v2.
1
+ You are Lead Fit Builder for Sellable create-campaign-v2.
2
2
 
3
3
  Your job starts only after find-leads has produced `lead-review.md` and
4
4
  `lead-sample.json`, and the lead source has been approved or auto-confirmed.
5
5
  Work only on the lead filter branch. Do not source new leads, draft messages,
6
- import leads, create campaigns, ask the user questions, or mutate live campaign
7
- state.
6
+ import leads, create campaigns, or ask the user questions. Your only live
7
+ campaign mutation is calling `save_rubrics` after the production rubrics are
8
+ ready.
8
9
 
9
10
  Required inputs:
10
11
 
11
12
  - `brief.md`
12
13
  - `lead-review.md`
13
14
  - `lead-sample.json`
15
+ - campaign state from the parent thread
16
+ - campaign table sample from the parent thread
14
17
 
15
18
  Required first steps:
16
19
 
17
20
  1. Read the three required inputs.
18
21
  2. Load the filter-leads reference before writing artifacts:
19
22
  `get_subskill_asset({ subskillName: "create-campaign-v2", assetPath: "references/filter-leads.md" })`.
23
+ 3. Treat campaign state and the campaign table sample as the input of record.
24
+ Disk files are context/debug aids, not durable state.
20
25
 
21
26
  Owned outputs:
22
27
 
23
- - `lead-filter.md`
24
- - `rubric.json` when the filter is confirmed and production-shaped rubrics are
25
- safe to write
28
+ - Durable campaign rubrics via `save_rubrics({ campaignOfferId, leadScoringRubrics })`
29
+ when the filter is confirmed and production-shaped rubrics are safe to write.
30
+ `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
26
33
 
27
34
  Do not write or modify:
28
35
 
@@ -46,12 +53,16 @@ Process:
46
53
  5. Keep source mechanics out of production rubrics. Engagement, provider,
47
54
  priority, or first-send ordering can inform prioritization, but they are not
48
55
  standalone ICP qualification rules.
49
- 6. Write `lead-filter.md` first. If status is `confirmed`, also write
50
- `rubric.json` with 2-5 production-shaped `leadScoringRubrics`.
56
+ 6. If status is `confirmed`, call `save_rubrics` with 2-5 production-shaped
57
+ active `leadScoringRubrics` before reporting success. If `save_rubrics`
58
+ 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.
51
61
 
52
62
  Return a concise final status with:
53
63
 
54
64
  - filter status: `confirmed`, `confirm-with-user`, or `revise-find-leads`
65
+ - whether `save_rubrics` succeeded and how many active rubrics were persisted
55
66
  - artifacts written
56
67
  - strongest keep rules
57
68
  - strongest exclusion rules
@@ -1,16 +1,19 @@
1
- You are the Post-Lead Message Scout for Sellable create-campaign-v2.
1
+ You are Message Draft Builder for Sellable create-campaign-v2.
2
2
 
3
3
  Your job starts only after find-leads has produced `lead-review.md` and
4
4
  `lead-sample.json`, and the lead source has been approved or auto-confirmed.
5
5
  Work only on the message generation branch. Do not source new leads, create lead
6
6
  filters, import leads, create campaigns, ask the user questions, or mutate live
7
- campaign state.
7
+ campaign state. Do not call `update_campaign_brief`; the main thread owns the
8
+ approval write.
8
9
 
9
10
  Required inputs:
10
11
 
11
12
  - `brief.md`
12
13
  - `lead-review.md`
13
14
  - `lead-sample.json`
15
+ - campaign state from the parent thread
16
+ - campaign table sample from the parent thread
14
17
 
15
18
  Required first steps:
16
19
 
@@ -18,12 +21,16 @@ Required first steps:
18
21
  2. Load 100% of the real generate-messages prompt with chunked
19
22
  `get_subskill_prompt({ subskillName: "generate-messages", offset, limit })`
20
23
  calls until `hasMore` is false.
24
+ 3. Treat campaign state and the campaign table sample as the input of record.
25
+ Disk files are context/debug aids, not durable state.
21
26
 
22
27
  Owned outputs:
23
28
 
24
- - `message-validation.md`
25
- - optional `message-prep.md`
26
- - optional `message-candidate-drafts.md`
29
+ - proposed template using supported `{{...}}` tokens
30
+ - one sample message rendered from the proposed template
31
+ - `message-validation.md` debug artifact
32
+ - optional `message-prep.md` debug artifact
33
+ - optional `message-candidate-drafts.md` debug artifact
27
34
 
28
35
  Do not write or modify:
29
36
 
@@ -48,13 +55,21 @@ Process:
48
55
  does not exist yet, choose probable good-fit rows from `lead-sample.json` and
49
56
  mark the final reconciliation as pending.
50
57
  5. Write `message-validation.md`. Do not write `message-review.md`; the parent
51
- thread owns the joined review after both post-lead scouts finish.
58
+ thread owns the joined review after both builders finish.
59
+ 6. Return the proposed template and one sample message for approval. Do not
60
+ mutate campaignBrief. The main thread must show readable filters with
61
+ reasons, show one sample message, ask the user to approve or edit, then write
62
+ the approved template with `update_campaign_brief` only after approval.
63
+ 7. If the user edits the proposal, the main thread updates the proposal and asks
64
+ again. Do not queue enrichment, validation rows, or Generate Message work
65
+ until the approved template write succeeds.
52
66
 
53
67
  Return a concise final status with:
54
68
 
55
69
  - artifacts written
56
70
  - whether the full generate-messages prompt was loaded
57
71
  - lead sample basis used
72
+ - proposed template and one sample message
58
73
  - selected winner summary
59
74
  - whether final reconciliation with `lead-filter.md` is complete or pending
60
75
 
@@ -154,25 +154,33 @@
154
154
  "name": "post-find-leads-filter-scout",
155
155
  "kind": "post-find-leads-scout",
156
156
  "promptFile": "post-find-leads-filter-scout.md",
157
- "displayName": "Post-Lead Filter Scout",
157
+ "displayName": "Lead Fit Builder",
158
158
  "target": "filter-leads",
159
- "inputs": ["brief.md", "lead-review.md", "lead-sample.json"],
160
- "producesArtifacts": ["lead-filter.md"],
161
- "optionalProducesArtifacts": ["rubric.json"],
159
+ "inputs": [
160
+ "brief.md",
161
+ "lead-review.md",
162
+ "lead-sample.json"
163
+ ],
164
+ "producesArtifacts": [
165
+ "lead-filter.md"
166
+ ],
167
+ "optionalProducesArtifacts": [
168
+ "rubric.json"
169
+ ],
162
170
  "ownership": "lead quality, false-positive patterns, keep/exclude rules, ability-to-pay checks, and production rubric translation only",
163
171
  "codex": {
164
- "description": "Sellable post-find-leads scout for lead filtering and rubric generation after source approval.",
172
+ "description": "Lead Fit Builder for campaign-backed lead filtering and rubric persistence after source approval.",
165
173
  "model": "gpt-5.5",
166
174
  "modelReasoningEffort": "high",
167
175
  "sandboxMode": "workspace-write",
168
176
  "nicknameCandidates": [
169
- "Lead Filter Scout",
170
- "Filter Scout",
171
- "Rubric Scout"
177
+ "Lead Fit Builder",
178
+ "Fit Builder",
179
+ "Rubric Builder"
172
180
  ]
173
181
  },
174
182
  "claude": {
175
- "description": "Use proactively as a background Sellable post-find-leads scout after lead source approval to produce lead-filter.md and rubric.json from brief.md, lead-review.md, and lead-sample.json.",
183
+ "description": "Use proactively as Lead Fit Builder after lead source approval to persist campaign rubrics and write lead-filter debug artifacts from campaign state.",
176
184
  "model": "inherit",
177
185
  "background": true,
178
186
  "maxTurns": 8,
@@ -184,7 +192,8 @@
184
192
  "Grep",
185
193
  "Glob",
186
194
  "mcp__sellable__get_subskill_prompt",
187
- "mcp__sellable__get_subskill_asset"
195
+ "mcp__sellable__get_subskill_asset",
196
+ "mcp__sellable__save_rubrics"
188
197
  ]
189
198
  }
190
199
  },
@@ -193,28 +202,34 @@
193
202
  "name": "post-find-leads-message-scout",
194
203
  "kind": "post-find-leads-scout",
195
204
  "promptFile": "post-find-leads-message-scout.md",
196
- "displayName": "Post-Lead Message Scout",
205
+ "displayName": "Message Draft Builder",
197
206
  "target": "generate-messages",
198
- "inputs": ["brief.md", "lead-review.md", "lead-sample.json"],
199
- "producesArtifacts": ["message-validation.md"],
207
+ "inputs": [
208
+ "brief.md",
209
+ "lead-review.md",
210
+ "lead-sample.json"
211
+ ],
212
+ "producesArtifacts": [
213
+ "message-validation.md"
214
+ ],
200
215
  "optionalProducesArtifacts": [
201
216
  "message-prep.md",
202
217
  "message-candidate-drafts.md"
203
218
  ],
204
219
  "ownership": "proof inventory, token strategy, angle drafting, skeptical-prospect review, and selected winner only",
205
220
  "codex": {
206
- "description": "Sellable post-find-leads scout for dry-mode message generation after source approval.",
221
+ "description": "Message Draft Builder for campaign-backed template proposals and sample messages after source approval.",
207
222
  "model": "gpt-5.5",
208
223
  "modelReasoningEffort": "high",
209
224
  "sandboxMode": "workspace-write",
210
225
  "nicknameCandidates": [
211
- "Message Scout",
212
- "Message Generation Scout",
213
- "Copy Scout"
226
+ "Message Draft Builder",
227
+ "Draft Builder",
228
+ "Template Builder"
214
229
  ]
215
230
  },
216
231
  "claude": {
217
- "description": "Use proactively as a background Sellable post-find-leads scout after lead source approval to run dry-mode message generation from brief.md, lead-review.md, and lead-sample.json.",
232
+ "description": "Use proactively as Message Draft Builder after lead source approval to propose an approved-template candidate and sample message from campaign state.",
218
233
  "model": "inherit",
219
234
  "background": true,
220
235
  "maxTurns": 10,
@@ -4,11 +4,19 @@ Your job is to test whether active LinkedIn posts and engagers can produce a war
4
4
 
5
5
  Required first step:
6
6
 
7
- - Load the canonical provider prompt before searching: `get_provider_prompt({ provider: "signal-discovery", confirmed: true })`.
7
+ - Load the canonical provider prompt before searching. If the parent supplies a
8
+ draft `campaignOfferId`, call `get_provider_prompt({ provider:
9
+ "signal-discovery", campaignOfferId, confirmed: true })` and include that same
10
+ `campaignOfferId` in `search_signals` so the user can watch source work in the
11
+ campaign UI. Treat that as a campaign-attached persisted search; do not run a
12
+ post-mint search without the campaign ID. If no campaign ID is supplied, run
13
+ campaignless preview mode.
8
14
 
9
15
  Use the inherited Sellable MCP tools when available:
10
16
 
11
- - `search_signals` to find recent post lanes.
17
+ - `search_signals` to find recent post lanes. Include `campaignOfferId` whenever
18
+ the parent provides one so selected searches/lists stay attached to the
19
+ campaign.
12
20
  - `fetch_post_engagers` to sample engagers from selected posts.
13
21
 
14
22
  Process:
@@ -4,13 +4,20 @@ Your job is to test whether Prospeo can produce verified-contact scale for the c
4
4
 
5
5
  Required first step:
6
6
 
7
- - Load the canonical provider prompt before searching: `get_provider_prompt({ provider: "prospeo", confirmed: true })`.
7
+ - Load the canonical provider prompt before searching. If the parent supplies a
8
+ draft `campaignOfferId`, call `get_provider_prompt({ provider: "prospeo",
9
+ campaignOfferId, confirmed: true })` and include that same `campaignOfferId` in
10
+ `search_prospeo` so the user can watch source work in the campaign UI. If no
11
+ campaign ID is supplied, run campaignless preview mode. Treat post-mint
12
+ searches with `campaignOfferId` as campaign-attached persisted search tabs;
13
+ do not run a live campaign search without the campaign ID.
8
14
 
9
15
  Use the inherited Sellable MCP tools when available:
10
16
 
11
17
  - `load_csv_domains` when the parent supplies a CSV on disk and no `domainFilterId` exists.
12
18
  - `save_domain_filters` when the parent supplies pasted/raw include or exclude domains and no `domainFilterId` exists.
13
- - `search_prospeo` for campaignless people previews.
19
+ - `search_prospeo` for people previews. Include `campaignOfferId` whenever the
20
+ parent provides one so selected searches/lists stay attached to the campaign.
14
21
 
15
22
  Process:
16
23
 
@@ -4,12 +4,20 @@ Your job is to test whether Sales Navigator filters can produce a scalable, high
4
4
 
5
5
  Required first step:
6
6
 
7
- - Load the canonical provider prompt before searching: `get_provider_prompt({ provider: "sales-nav", confirmed: true })`.
7
+ - Load the canonical provider prompt before searching. If the parent supplies a
8
+ draft `campaignOfferId`, call `get_provider_prompt({ provider: "sales-nav",
9
+ campaignOfferId, confirmed: true })` and include that same `campaignOfferId` in
10
+ `search_sales_nav` so the user can watch source work in the campaign UI. If no
11
+ campaign ID is supplied, run campaignless preview mode. Treat post-mint
12
+ searches with `campaignOfferId` as campaign-attached persisted search tabs;
13
+ do not run a live campaign search without the campaign ID.
8
14
 
9
15
  Use the inherited Sellable MCP tools when available:
10
16
 
11
17
  - `lookup_sales_nav_filter` before any dynamic Sales Nav filter.
12
- - `search_sales_nav` for campaignless preview searches.
18
+ - `search_sales_nav` for preview searches. Include `campaignOfferId` whenever
19
+ the parent provides one so selected searches/lists stay attached to the
20
+ campaign.
13
21
 
14
22
  Process:
15
23
 
package/dist/server.js CHANGED
@@ -170,6 +170,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
170
170
  break;
171
171
  case "attach_recommended_sequence":
172
172
  result = await attachRecommendedSequence(args);
173
+ if (args?.campaignId) {
174
+ markCampaignContextDirty(args.campaignId, "attach_recommended_sequence");
175
+ }
173
176
  break;
174
177
  case "bootstrap_create_campaign":
175
178
  result = await bootstrapCreateCampaign(args);
@@ -194,7 +197,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
194
197
  case "update_campaign":
195
198
  result = await updateCampaign(args?.campaignId, args);
196
199
  if (result?._campaign?.id) {
197
- const entry = hydrateCampaignContextFromCampaign(result._campaign, "update_campaign");
200
+ const entry = hydrateCampaignContextFromCampaign(result._campaign, "update_campaign", args?.flowVersion === "v2" ? "v2" : "v1");
198
201
  result.navigation = entry.navState;
199
202
  delete result._campaign;
200
203
  }
@@ -204,6 +207,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
204
207
  break;
205
208
  case "start_campaign":
206
209
  result = await startCampaign(args?.campaignId);
210
+ if (args?.campaignId && result?.success) {
211
+ markCampaignContextDirty(args.campaignId, "start_campaign");
212
+ }
207
213
  break;
208
214
  case "pause_campaign":
209
215
  result = await pauseCampaign(args?.campaignId);
@@ -268,9 +268,9 @@ export async function bootstrapCreateCampaign(input = {}) {
268
268
  : "";
269
269
  const nextStep = safeToProceed
270
270
  ? resumeDetected
271
- ? `Bootstrap complete.${workspaceNotice} Resume using campaign context, 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.`
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. Then start with the create-campaign-brief interview stage. Do not call create_campaign until the v2 approval gate returns approve.`
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.`
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
@@ -128,6 +128,7 @@ export interface CreateCampaignInput {
128
128
  senderIds?: string[];
129
129
  }
130
130
  export interface UpdateCampaignInput {
131
+ flowVersion?: "v1" | "v2";
131
132
  offerPositioning?: unknown;
132
133
  campaignBrief?: unknown;
133
134
  leadSourceType?: string | null;
@@ -170,6 +171,7 @@ export declare const campaignToolDefinitions: ({
170
171
  enableICPFilters?: undefined;
171
172
  useMessagingTemplate?: undefined;
172
173
  rubric?: undefined;
174
+ flowVersion?: undefined;
173
175
  };
174
176
  required: never[];
175
177
  additionalProperties?: undefined;
@@ -204,6 +206,7 @@ export declare const campaignToolDefinitions: ({
204
206
  enableICPFilters?: undefined;
205
207
  useMessagingTemplate?: undefined;
206
208
  rubric?: undefined;
209
+ flowVersion?: undefined;
207
210
  };
208
211
  required: string[];
209
212
  additionalProperties?: undefined;
@@ -281,6 +284,7 @@ export declare const campaignToolDefinitions: ({
281
284
  enableICPFilters?: undefined;
282
285
  useMessagingTemplate?: undefined;
283
286
  rubric?: undefined;
287
+ flowVersion?: undefined;
284
288
  };
285
289
  required: string[];
286
290
  additionalProperties: boolean;
@@ -353,6 +357,7 @@ export declare const campaignToolDefinitions: ({
353
357
  enableICPFilters?: undefined;
354
358
  useMessagingTemplate?: undefined;
355
359
  rubric?: undefined;
360
+ flowVersion?: undefined;
356
361
  };
357
362
  required: never[];
358
363
  additionalProperties?: undefined;
@@ -416,6 +421,11 @@ export declare const campaignToolDefinitions: ({
416
421
  type: string;
417
422
  description: string;
418
423
  };
424
+ flowVersion: {
425
+ type: string;
426
+ enum: string[];
427
+ description: string;
428
+ };
419
429
  limit?: undefined;
420
430
  tableId?: undefined;
421
431
  leadLimit?: undefined;
@@ -462,6 +472,7 @@ export declare const campaignToolDefinitions: ({
462
472
  enableICPFilters?: undefined;
463
473
  useMessagingTemplate?: undefined;
464
474
  rubric?: undefined;
475
+ flowVersion?: undefined;
465
476
  };
466
477
  required: string[];
467
478
  additionalProperties?: undefined;
@@ -485,6 +496,7 @@ export interface UpdateCampaignResult {
485
496
  name: string;
486
497
  currentStep: string | null;
487
498
  updatedAt: string;
499
+ remediation?: string;
488
500
  navigation?: unknown;
489
501
  _campaign?: CampaignOfferNavigation;
490
502
  }
@@ -139,7 +139,7 @@ export const campaignToolDefinitions = [
139
139
  },
140
140
  clientProspectId: {
141
141
  type: "string",
142
- description: "Use this for existing sender prospects. For brand-new senders, pass `senderLinkedinUrl` and the backend will attempt sender lookup/creation during campaign mint.",
142
+ description: "Use this for existing sender prospects. For brand-new senders, pass `senderLinkedinUrl` and the backend will attempt sender lookup/creation during campaign creation.",
143
143
  },
144
144
  // `senderLinkedinUrl` (not `linkedinUrl`) to disambiguate from
145
145
  // prospect-side `linkedinUrl` fields used in other tools (research-prospect etc.).
@@ -254,6 +254,11 @@ export const campaignToolDefinitions = [
254
254
  type: "array",
255
255
  description: "Lead scoring rubric entries",
256
256
  },
257
+ flowVersion: {
258
+ type: "string",
259
+ enum: ["v1", "v2"],
260
+ description: "Framework flow version for returned navigation diagnostics (default: v1).",
261
+ },
257
262
  },
258
263
  required: ["campaignId"],
259
264
  },
@@ -618,7 +623,7 @@ export async function createCampaign(input) {
618
623
  `or all create fields. Missing: ${missing.join(", ")}.\n\n` +
619
624
  "Remediation:\n" +
620
625
  '- For full workflow, call get_subskill_prompt({ subskillName: "create-campaign" }) and follow it.\n' +
621
- "- For net-new campaign creation: run the `research-sender` flow first, call `complete_sender_research`, then pass either `clientProspectId` (preferred) or `senderLinkedinUrl` and let the backend resolve sender context at mint time.\n" +
626
+ "- For net-new campaign creation: run the `research-sender` flow first, call `complete_sender_research`, then pass either `clientProspectId` (preferred) or `senderLinkedinUrl` and let the backend resolve sender context at campaign creation time.\n" +
622
627
  "- For resume, call create_campaign with campaignId only.");
623
628
  }
624
629
  assertNetNewCreateCampaignResearchReady();
@@ -653,7 +658,7 @@ export async function createCampaign(input) {
653
658
  if (isAutonomousMessageMint &&
654
659
  !explicitlyAllowsAiGenerated &&
655
660
  !hasTemplateToken) {
656
- throw new Error("VALIDATION_ERROR: campaignBrief must include at least one supported {{token}} before minting into autonomous messaging.\n\n" +
661
+ throw new Error("VALIDATION_ERROR: campaignBrief must include at least one supported {{token}} before entering autonomous messaging.\n\n" +
657
662
  "Why this is blocked:\n" +
658
663
  "- Generate Message enters template mode only when campaignBrief.content contains {{...}}.\n" +
659
664
  "- Without a token, the message cascade uses freeform AI generation and may rewrite the approved message.\n\n" +
@@ -707,7 +712,11 @@ export async function createCampaign(input) {
707
712
  }
708
713
  export async function updateCampaign(campaignId, input) {
709
714
  const api = getApi();
710
- const { campaignId: _ignore, interactionMode, ...updates } = input;
715
+ const { campaignId: _ignore, interactionMode, flowVersion: _flowVersion, senderIds, currentStep, ...updates } = input;
716
+ const hasSenderIds = Object.prototype.hasOwnProperty.call(input, "senderIds");
717
+ if (!hasSenderIds && currentStep !== undefined) {
718
+ updates.currentStep = currentStep;
719
+ }
711
720
  // Normalize campaignBrief: if a plain string is passed, wrap it in the
712
721
  // { name, content } structure the UI expects. createCampaign already does
713
722
  // this, but updateCampaign was passing the string straight through.
@@ -747,13 +756,39 @@ export async function updateCampaign(campaignId, input) {
747
756
  !updates.currentStep) {
748
757
  updates.currentStep = "filter-rules";
749
758
  }
750
- const result = await api.put(`/api/v2/campaign-offers/${campaignId}`, updates);
759
+ let result = null;
760
+ let senderRouteResult = null;
761
+ const shouldPreserveInteractionModePut = typeof interactionMode === "string" &&
762
+ interactionMode.trim().length > 0 &&
763
+ !hasSenderIds &&
764
+ Object.keys(updates).length === 0;
765
+ if (Object.keys(updates).length > 0 || shouldPreserveInteractionModePut) {
766
+ result = await api.put(`/api/v2/campaign-offers/${campaignId}`, updates);
767
+ }
768
+ if (hasSenderIds) {
769
+ const senderList = senderIds;
770
+ const senderCurrentStep = Array.isArray(senderList) && senderList.length === 0
771
+ ? "settings"
772
+ : currentStep;
773
+ const senderPayload = { senderIds: senderList };
774
+ if (senderCurrentStep !== undefined) {
775
+ senderPayload.currentStep = senderCurrentStep;
776
+ }
777
+ senderRouteResult = await api.patch(`/api/v3/campaigns/${campaignId}/senders`, senderPayload);
778
+ result = await api.get(`/api/v2/campaign-offers/${campaignId}`);
779
+ }
780
+ if (!result) {
781
+ result = await api.put(`/api/v2/campaign-offers/${campaignId}`, updates);
782
+ }
751
783
  // Serialize to only essential fields for context efficiency
752
784
  return {
753
785
  id: result.id,
754
786
  name: result.name,
755
787
  currentStep: result.currentStep,
756
788
  updatedAt: result.updatedAt,
789
+ ...(senderRouteResult?.remediation
790
+ ? { remediation: senderRouteResult.remediation }
791
+ : {}),
757
792
  // Provide full campaign snapshot for server-side context hydration.
758
793
  _campaign: result,
759
794
  };
@@ -22,8 +22,8 @@ type CampaignContextEntry = {
22
22
  dirty: boolean;
23
23
  dirtyReasons: string[];
24
24
  };
25
- export declare function hydrateCampaignContextFromCampaign(campaign: CampaignOfferNavigation, reason: string): CampaignContextEntry;
26
- export declare function hydrateCampaignContextFromNavigation(navState: Awaited<ReturnType<typeof getCampaignNavigationState>>, reason: string): CampaignContextEntry;
25
+ export declare function hydrateCampaignContextFromCampaign(campaign: CampaignOfferNavigation, reason: string, flowVersion?: "v1" | "v2"): CampaignContextEntry;
26
+ export declare function hydrateCampaignContextFromNavigation(navState: Awaited<ReturnType<typeof getCampaignNavigationState>>, reason: string, flowVersion?: "v1" | "v2"): CampaignContextEntry;
27
27
  export declare function markCampaignContextDirty(campaignId: string, reason: string): void;
28
28
  export declare const contextToolDefinitions: {
29
29
  name: string;
@@ -62,8 +62,8 @@ export declare function getCampaignContext(input: GetCampaignContextInput): Prom
62
62
  dirtyReasons: string[];
63
63
  navigation: {
64
64
  campaignId: string;
65
- computedStep: "campaign-created" | "pick-provider" | "provider-search" | "confirm-lead-list";
66
- blockedAt: ("campaign-created" | "pick-provider" | "provider-search" | "confirm-lead-list") | null;
65
+ computedStep: "running" | "messages" | "settings" | "sequence" | "send" | "campaign-created" | "pick-provider" | "provider-search" | "confirm-lead-list" | "filter-rules";
66
+ blockedAt: ("running" | "messages" | "settings" | "sequence" | "send" | "campaign-created" | "pick-provider" | "provider-search" | "confirm-lead-list" | "filter-rules") | null;
67
67
  missing: string[];
68
68
  expectedHeadlessStep: string | null;
69
69
  headlessCurrentStep: string | null;
@@ -79,6 +79,12 @@ export declare function getCampaignContext(input: GetCampaignContextInput): Prom
79
79
  leadSourceProvider: string | null;
80
80
  selectedLeadListId: string | null;
81
81
  providerCurrentStep: string | null;
82
+ enableICPFilters: boolean | null;
83
+ activeRubricCount: number;
84
+ hasApprovedMessageTemplate: boolean;
85
+ senderCount: number;
86
+ hasSequenceTemplate: boolean;
87
+ campaignStatus: string | null;
82
88
  };
83
89
  warnings: string[];
84
90
  };
@@ -32,7 +32,7 @@ function deriveProviderConfig(framework, navState) {
32
32
  return null;
33
33
  return framework.providers[providerId] ?? null;
34
34
  }
35
- export function hydrateCampaignContextFromCampaign(campaign, reason) {
35
+ export function hydrateCampaignContextFromCampaign(campaign, reason, flowVersion = "v1") {
36
36
  logNavigationDebug("context.hydrate_from_campaign.start", {
37
37
  campaignId: campaign.id,
38
38
  reason,
@@ -40,7 +40,6 @@ export function hydrateCampaignContextFromCampaign(campaign, reason) {
40
40
  workflowTableId: campaign.workflowTableId ?? null,
41
41
  leadSourceProvider: campaign.leadSourceProvider ?? null,
42
42
  }, campaign.id);
43
- const flowVersion = "v1";
44
43
  const framework = getFrameworkSnapshot(flowVersion);
45
44
  const navState = computeCampaignNavigationStateFromCampaign(campaign, {
46
45
  includeTableCheck: false,
@@ -70,7 +69,7 @@ export function hydrateCampaignContextFromCampaign(campaign, reason) {
70
69
  }, campaign.id);
71
70
  return entry;
72
71
  }
73
- export function hydrateCampaignContextFromNavigation(navState, reason) {
72
+ export function hydrateCampaignContextFromNavigation(navState, reason, flowVersion = "v1") {
74
73
  logNavigationDebug("context.hydrate_from_navigation.start", {
75
74
  campaignId: navState.campaignId,
76
75
  reason,
@@ -78,7 +77,6 @@ export function hydrateCampaignContextFromNavigation(navState, reason) {
78
77
  blockedAt: navState.blockedAt,
79
78
  missing: navState.missing,
80
79
  }, navState.campaignId);
81
- const flowVersion = "v1";
82
80
  const framework = getFrameworkSnapshot(flowVersion);
83
81
  const providerConfig = deriveProviderConfig(framework, navState);
84
82
  const entry = {
@@ -178,6 +176,12 @@ export function markCampaignContextDirty(campaignId, reason) {
178
176
  leadSourceProvider: null,
179
177
  selectedLeadListId: null,
180
178
  providerCurrentStep: null,
179
+ enableICPFilters: null,
180
+ activeRubricCount: 0,
181
+ hasApprovedMessageTemplate: false,
182
+ senderCount: 0,
183
+ hasSequenceTemplate: false,
184
+ campaignStatus: null,
181
185
  },
182
186
  warnings: [],
183
187
  },
@@ -114,6 +114,8 @@ export type ImportLeadsInput = {
114
114
  headlineICPCriteria?: string[];
115
115
  rubricGuidelines?: string[];
116
116
  };
117
+ type ImportLeadProvider = NonNullable<ImportLeadsInput["provider"]>;
118
+ type Phase126ImportProvider = Exclude<ImportLeadProvider, "apollo">;
117
119
  export type CancelLeadImportInput = {
118
120
  campaignOfferId: string;
119
121
  tableId: string;
@@ -2331,7 +2333,7 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
2331
2333
  createdLeadList?: undefined;
2332
2334
  jobResult?: undefined;
2333
2335
  } | {
2334
- provider: "signal-discovery" | "apollo" | "sales-nav" | "prospeo";
2336
+ provider: Phase126ImportProvider;
2335
2337
  leadListId: string;
2336
2338
  existingLeadListId: string;
2337
2339
  needsModeSelection: boolean;
@@ -2379,7 +2381,7 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
2379
2381
  createdLeadList?: undefined;
2380
2382
  jobResult?: undefined;
2381
2383
  } | {
2382
- provider: "apollo" | "sales-nav" | "prospeo";
2384
+ provider: "sales-nav" | "prospeo";
2383
2385
  leadListId: string;
2384
2386
  existingLeadListId: string;
2385
2387
  needsModeSelection: boolean;
@@ -2411,7 +2413,7 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
2411
2413
  createdLeadList?: undefined;
2412
2414
  jobResult?: undefined;
2413
2415
  } | {
2414
- provider: "apollo" | "sales-nav" | "prospeo";
2416
+ provider: "sales-nav" | "prospeo";
2415
2417
  leadListId: string;
2416
2418
  createdLeadList: any;
2417
2419
  jobResult: any;
@@ -2471,3 +2473,4 @@ export declare function setHeadlineICPCriteria(input: SetHeadlineICPCriteriaInpu
2471
2473
  criteriaCount: number;
2472
2474
  message: string;
2473
2475
  }>;
2476
+ export {};