@sellable/mcp 0.1.950 → 0.1.952

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.
@@ -3,14 +3,14 @@
3
3
  * manifests. Keep this free of tool implementation imports so Next can bundle
4
4
  * it; registry parity is enforced in sellable-agent-tool-policy.test.ts.
5
5
  */
6
- export declare const SELLABLE_MCP_TOOL_NAMES: readonly ["add_column", "add_on_demand_leads", "add_rubric_item", "add_teammate", "add_to_comment_campaign", "add_to_connection_campaign", "add_to_inmail_campaign", "add_to_post_campaign", "archive_campaign", "attach_recommended_sequence", "attach_sequence", "bootstrap_create_campaign", "bootstrap_engage", "bootstrap_engage_multi", "bootstrap_find_leads", "bulk_enrich_with_prospeo", "calculate_linkedin_hook_preview", "cancel_campaign_message_preparation", "cancel_find_leads", "cancel_lead_import", "capture_post_idea", "check_inbox_reply_eligibility", "check_rubric", "commit_blueprint", "complete_sender_research", "confirm_harvest_job_companies", "confirm_lead_list", "confirm_prospeo_company_accounts", "copy_sender_config", "create_campaign", "create_on_demand_campaign", "create_on_demand_table", "create_table", "create_workflow_table", "create_workspace", "customer_program", "decide_agent_endpoint_proposal", "delete_column", "delete_rubric_item", "draft_rubrics", "duplicate_campaign", "enrich_with_prospeo", "export_table_csv", "export_workspace_csv", "fetch_company", "fetch_company_posts", "fetch_linkedin_posts", "fetch_linkedin_profile", "fetch_post_engagers", "fill_campaign_horizon", "get_active_workspace", "get_agent_endpoint_run", "get_agent_generation_routing", "get_agent_soul", "get_auth_status", "get_campaign", "get_campaign_context", "get_campaign_framework", "get_campaign_message_preparation_status", "get_campaign_messages_preview", "get_campaign_navigation_state", "get_campaign_refill_state", "get_campaign_table_schema", "get_campaign_waterfall", "get_campaigns", "get_column_schema", "get_company_info", "get_engage_memory", "get_engage_state", "get_engaged_posts", "get_find_leads_run", "get_inbox_thread", "get_integration_connection_status", "get_linkedin_profile", "get_or_create_direct_campaign_table", "get_post_draft", "get_post_find_leads_scout_registry", "get_post_idea", "get_prospeo_credits", "get_provider_prompt", "get_published_post", "get_refill_target_plan", "get_rows", "get_rows_minimal", "get_runtime_identity", "get_scheduler_fill_capacity", "get_sender", "get_sender_routing", "get_source_scout_registry", "get_subskill_asset", "get_subskill_prompt", "get_table_rows", "get_user_posts", "get_workspace", "import_leads", "init_on_demand_sequence", "integrations_call_tool", "integrations_describe_tool", "integrations_list_tools", "list_agent_endpoint_runs", "list_agent_endpoints", "list_column_types", "list_dnc_entries", "list_post_draft_iterations", "list_post_drafts", "list_post_ideas", "list_published_posts", "list_senders", "list_subskill_prompts", "list_tables", "list_workspaces", "load_csv_dnc_entries", "load_csv_domains", "load_csv_linkedin_leads", "lookup_sales_nav_filter", "mark_post_published", "migrate_flat_configs", "opportunity_sequence_enroll", "opportunity_sequence_next_page", "opportunity_sequence_record", "pause_campaign", "pause_direct_campaign", "pause_on_demand_campaign", "preflight_find_leads_provider", "prepare_campaign_ab_test", "publish_agent_endpoint", "queue_campaign_cells", "queue_cells", "record_campaign_review_batch", "record_engage_proven_search", "refill_sends", "refill_v3_advance", "refill_v3_continue", "refill_v3_edit_selected_campaign", "refill_v3_fill_ready", "refill_v3_world_state", "refresh_paid_inmail_credits", "refresh_sender_engagement", "reissue_find_leads_watch_link", "render_linkedin_post_preview", "reorder_columns", "resolve_campaign_fill_route", "revise_message_template_and_rerun", "rotate_agent_endpoint_key", "run_scheduler_sweep", "save_domain_filters", "save_hook_research", "save_post_draft", "save_rubrics", "search_apollo", "search_available_integrations", "search_engagement_posts", "search_harvest_jobs", "search_inbox_threads", "search_prospeo", "search_prospeo_companies", "search_sales_nav", "search_signals", "search_subskill_prompts", "select_campaign_cells", "select_necessary_rubrics", "select_promising_posts", "send_inbox_draft", "send_inbox_manual_reply", "set_active_workspace", "set_agent_generation_flags", "set_campaign_waterfall_order", "set_company_info", "set_engage_state", "set_engage_style_guide", "set_headline_icp_criteria", "set_sender_calendar_link", "set_sender_routing", "setup_evergreen_campaigns", "start_campaign", "start_campaign_message_preparation", "start_cli_login", "start_direct_campaign", "start_integration_connection", "start_on_demand_campaign", "stop_agent_endpoint_run", "update_agent_soul", "update_campaign", "update_campaign_brief", "update_cell", "update_column", "update_find_leads_run", "update_inbox_draft", "update_post_draft", "update_published_post_metrics", "update_rubric_item", "upsert_engage_tracked_person", "upsert_rubric", "validate_campaign1_kickoff_handoff", "verify_table_row", "wait_for_campaign_processing", "wait_for_campaign_table_ready", "wait_for_cli_login", "wait_for_find_leads_run", "wait_for_lead_list_ready", "wait_for_rubric_results"];
6
+ export declare const SELLABLE_MCP_TOOL_NAMES: readonly ["add_column", "add_on_demand_leads", "add_rubric_item", "add_teammate", "add_to_comment_campaign", "add_to_connection_campaign", "add_to_inmail_campaign", "add_to_post_campaign", "archive_campaign", "attach_recommended_sequence", "attach_sequence", "bootstrap_create_campaign", "bootstrap_engage", "bootstrap_engage_multi", "bootstrap_find_leads", "bulk_enrich_with_prospeo", "calculate_linkedin_hook_preview", "cancel_campaign_message_preparation", "cancel_find_leads", "cancel_lead_import", "capture_post_idea", "check_inbox_reply_eligibility", "check_rubric", "commit_blueprint", "complete_sender_research", "confirm_harvest_job_companies", "confirm_lead_list", "confirm_prospeo_company_accounts", "copy_sender_config", "create_campaign", "create_on_demand_campaign", "create_on_demand_table", "create_table", "create_workflow_table", "create_workspace", "customer_program", "decide_agent_endpoint_proposal", "delete_column", "delete_rubric_item", "draft_rubrics", "duplicate_campaign", "enrich_with_prospeo", "export_table_csv", "export_workspace_csv", "fetch_company", "fetch_company_posts", "fetch_linkedin_posts", "fetch_linkedin_profile", "fetch_post_engagers", "fill_campaign_horizon", "get_active_workspace", "get_agent_endpoint_run", "get_agent_generation_routing", "get_agent_soul", "get_auth_status", "get_campaign", "get_campaign_context", "get_campaign_framework", "get_campaign_message_preparation_status", "get_campaign_messages_preview", "get_campaign_navigation_state", "get_campaign_refill_state", "get_campaign_table_schema", "get_campaign_waterfall", "get_campaigns", "get_column_schema", "get_company_info", "get_engage_memory", "get_engage_state", "get_engaged_posts", "get_find_leads_run", "get_inbox_thread", "get_integration_connection_status", "get_linkedin_profile", "get_or_create_direct_campaign_table", "get_post_draft", "get_post_find_leads_scout_registry", "get_post_idea", "get_prospeo_credits", "get_provider_prompt", "get_published_post", "get_refill_target_plan", "get_rows", "get_rows_minimal", "get_runtime_identity", "get_scheduler_fill_capacity", "get_sender", "get_sender_routing", "get_source_scout_registry", "get_subskill_asset", "get_subskill_prompt", "get_table_rows", "get_user_posts", "get_workspace", "import_leads", "init_on_demand_sequence", "integrations_call_tool", "integrations_describe_tool", "integrations_list_tools", "list_agent_endpoint_runs", "list_agent_endpoints", "list_column_types", "list_dnc_entries", "list_post_draft_iterations", "list_post_drafts", "list_post_ideas", "list_published_posts", "list_senders", "list_subskill_prompts", "list_tables", "list_workspaces", "load_csv_dnc_entries", "load_csv_domains", "load_csv_linkedin_leads", "lookup_sales_nav_filter", "mark_post_published", "migrate_flat_configs", "opportunity_sequence_enroll", "opportunity_sequence_next_page", "opportunity_sequence_record", "pause_campaign", "pause_direct_campaign", "pause_on_demand_campaign", "preflight_find_leads_provider", "prepare_campaign_ab_test", "publish_agent_endpoint", "queue_campaign_cells", "queue_cells", "record_campaign_review_batch", "record_engage_proven_search", "refill_sends", "refill_v3_advance", "refill_v3_continue", "refill_v3_edit_selected_campaign", "refill_v3_fill_ready", "refill_v3_world_state", "refresh_paid_inmail_credits", "refresh_sender_engagement", "reissue_find_leads_watch_link", "render_linkedin_post_preview", "reorder_columns", "resolve_campaign_fill_route", "revise_message_template_and_rerun", "rotate_agent_endpoint_key", "run_scheduler_sweep", "save_domain_filters", "save_hook_research", "save_post_draft", "save_rubrics", "search_apollo", "search_available_integrations", "search_engagement_posts", "search_harvest_jobs", "search_inbox_threads", "search_prospeo", "search_prospeo_companies", "search_sales_nav", "search_signals", "search_subskill_prompts", "select_campaign_cells", "select_necessary_rubrics", "select_promising_posts", "send_inbox_draft", "send_inbox_manual_reply", "set_active_workspace", "set_agent_generation_flags", "set_campaign_waterfall_order", "set_company_info", "set_engage_state", "set_engage_style_guide", "set_headline_icp_criteria", "set_sender_calendar_link", "set_sender_context", "set_sender_routing", "setup_evergreen_campaigns", "start_campaign", "start_campaign_message_preparation", "start_cli_login", "start_direct_campaign", "start_integration_connection", "start_on_demand_campaign", "stop_agent_endpoint_run", "update_agent_soul", "update_campaign", "update_campaign_brief", "update_cell", "update_column", "update_find_leads_run", "update_inbox_draft", "update_post_draft", "update_published_post_metrics", "update_rubric_item", "upsert_engage_tracked_person", "upsert_rubric", "validate_campaign1_kickoff_handoff", "verify_table_row", "wait_for_campaign_processing", "wait_for_campaign_table_ready", "wait_for_cli_login", "wait_for_find_leads_run", "wait_for_lead_list_ready", "wait_for_rubric_results"];
7
7
  /**
8
8
  * Explicit mutation classification lives beside (but outside) the flat tool
9
9
  * registry. Neither this list nor `isMutatingTool` is an input to
10
10
  * `compileAgentToolPolicy`, so editing it cannot move the fleet-pinned
11
11
  * `policyHash`.
12
12
  */
13
- export declare const MUTATING_TOOL_NAMES: readonly ["create_table", "publish_agent_endpoint", "rotate_agent_endpoint_key", "decide_agent_endpoint_proposal", "stop_agent_endpoint_run", "pause_campaign", "archive_campaign", "update_campaign", "fill_campaign_horizon", "set_campaign_waterfall_order", "set_sender_routing", "set_agent_generation_flags", "send_inbox_draft", "send_inbox_manual_reply", "load_csv_dnc_entries", "update_cell", "queue_cells", "check_rubric", "queue_campaign_cells", "start_campaign_message_preparation", "opportunity_sequence_enroll", "opportunity_sequence_record", "refill_v3_fill_ready", "refill_v3_edit_selected_campaign", "refill_v3_advance", "refill_v3_continue"];
13
+ export declare const MUTATING_TOOL_NAMES: readonly ["create_table", "publish_agent_endpoint", "rotate_agent_endpoint_key", "decide_agent_endpoint_proposal", "stop_agent_endpoint_run", "pause_campaign", "archive_campaign", "update_campaign", "fill_campaign_horizon", "set_campaign_waterfall_order", "set_sender_routing", "set_sender_context", "set_agent_generation_flags", "send_inbox_draft", "send_inbox_manual_reply", "load_csv_dnc_entries", "update_cell", "queue_cells", "check_rubric", "queue_campaign_cells", "start_campaign_message_preparation", "opportunity_sequence_enroll", "opportunity_sequence_record", "refill_v3_fill_ready", "refill_v3_edit_selected_campaign", "refill_v3_advance", "refill_v3_continue"];
14
14
  export declare function isMutatingTool(name: string): boolean;
15
15
  export declare const CROSS_WORKSPACE_EXPLICIT_WORKSPACE_REQUIRED: {
16
16
  readonly code: "cross_workspace_explicit_workspace_required";
@@ -176,6 +176,7 @@ export const SELLABLE_MCP_TOOL_NAMES = [
176
176
  "set_engage_style_guide",
177
177
  "set_headline_icp_criteria",
178
178
  "set_sender_calendar_link",
179
+ "set_sender_context",
179
180
  "set_sender_routing",
180
181
  "setup_evergreen_campaigns",
181
182
  "start_campaign",
@@ -227,6 +228,9 @@ export const MUTATING_TOOL_NAMES = [
227
228
  "fill_campaign_horizon",
228
229
  "set_campaign_waterfall_order",
229
230
  "set_sender_routing",
231
+ // A sender's personal context is what the message writer says about a real
232
+ // person, so a cross-workspace caller must name the workspace it means.
233
+ "set_sender_context",
230
234
  "set_agent_generation_flags",
231
235
  "send_inbox_draft",
232
236
  "send_inbox_manual_reply",
package/dist/server.js CHANGED
@@ -34,9 +34,9 @@ import { handleReorderColumns } from "./tools/column-reorder.js";
34
34
  import { handleGetColumnSchema, handleListColumnTypes, } from "./tools/column-schema.js";
35
35
  import { handleUpdateColumn } from "./tools/column-update.js";
36
36
  import { getCompanyInfoTool, setCompanyInfoTool, } from "./tools/company-info.js";
37
- import { customerProgramTool } from "./tools/customer-program.js";
38
37
  import { calculateLinkedInHookPreviewTool, capturePostIdeaTool, getPostDraftTool, getPostIdeaTool, getPublishedPostTool, listPostDraftIterationsTool, listPostDraftsTool, listPostIdeasTool, listPublishedPostsTool, markPostPublishedTool, renderLinkedInPostPreviewTool, saveHookResearchTool, savePostDraftTool, updatePostDraftTool, updatePublishedPostMetricsTool, } from "./tools/content-posts.js";
39
38
  import { getCampaignContext, hydrateCampaignContextFromCampaign, markCampaignContextDirty, } from "./tools/context.js";
39
+ import { customerProgramTool } from "./tools/customer-program.js";
40
40
  import { addToCommentCampaign, addToConnectionCampaign, addToInmailCampaign, addToPostCampaign, getEngagedPosts, getOrCreateDirectCampaignTable, pauseDirectCampaign, startDirectCampaign, } from "./tools/direct-campaigns.js";
41
41
  import { bootstrapEngage, bootstrapEngageMulti, } from "./tools/engage-bootstrap.js";
42
42
  import { searchEngagementPosts } from "./tools/engage-discovery.js";
@@ -75,7 +75,7 @@ import { getRuntimeIdentityTool } from "./tools/runtime-identity.js";
75
75
  import { getSchedulerFillCapacity } from "./tools/scheduler-fill-capacity.js";
76
76
  import { runSchedulerSweep } from "./tools/scheduler-run.js";
77
77
  import { getSenderRoutingTool, setSenderRoutingTool, } from "./tools/sender-routing.js";
78
- import { getSender, listSenders, refreshPaidInmailCredits, setSenderCalendarLink, } from "./tools/senders.js";
78
+ import { getSender, listSenders, refreshPaidInmailCredits, setSenderCalendarLink, setSenderContext, } from "./tools/senders.js";
79
79
  import { attachRecommendedSequence, attachSequence, createWorkflowTable, } from "./tools/sequencer.js";
80
80
  import { setupEvergreenCampaigns } from "./tools/setup-evergreen-campaigns.js";
81
81
  import { createTable, exportTableCsv, listTables } from "./tools/tables.js";
@@ -201,8 +201,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
201
201
  ok: false,
202
202
  error: boundary.authorization.code,
203
203
  decision: "DENY",
204
- ...(boundary.authorization.code === "managed_agent_auth_unavailable"
205
- ? { message: "Managed authentication is unavailable. The runtime supervisor must restore it; do not copy personal credentials, start browser login, or retry a Calendar mutation automatically." }
204
+ ...(boundary.authorization.code ===
205
+ "managed_agent_auth_unavailable"
206
+ ? {
207
+ message: "Managed authentication is unavailable. The runtime supervisor must restore it; do not copy personal credentials, start browser login, or retry a Calendar mutation automatically.",
208
+ }
206
209
  : {}),
207
210
  }),
208
211
  },
@@ -862,6 +865,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
862
865
  case "set_sender_calendar_link":
863
866
  result = await setSenderCalendarLink(args);
864
867
  break;
868
+ case "set_sender_context":
869
+ result = await setSenderContext(args);
870
+ break;
865
871
  case "refresh_paid_inmail_credits":
866
872
  result = await refreshPaidInmailCredits(args);
867
873
  break;
@@ -1,8 +1,8 @@
1
1
  import { type AuthStatus } from "./auth.js";
2
2
  import { getCampaignContext } from "./context.js";
3
3
  import { getCampaignFramework } from "./framework.js";
4
- import { type ListSubskillPromptsResponse, type SubskillPromptResponse } from "./prompts.js";
5
4
  import { type CampaignModelQualityResult } from "./model-quality.js";
5
+ import { type ListSubskillPromptsResponse, type SubskillPromptResponse } from "./prompts.js";
6
6
  type BootstrapCreateCampaignInput = {
7
7
  campaignId?: string;
8
8
  flowVersion?: "v1" | "v2";
@@ -2,8 +2,8 @@ import { resetApi } from "../api.js";
2
2
  import { getAuthStatus } from "./auth.js";
3
3
  import { getCampaignContext } from "./context.js";
4
4
  import { getCampaignFramework } from "./framework.js";
5
- import { getSubskillPrompt, listSubskillPrompts, } from "./prompts.js";
6
5
  import { evaluateCampaignModelQuality, getCampaignModelMinimumSummary, } from "./model-quality.js";
6
+ import { getSubskillPrompt, listSubskillPrompts, } from "./prompts.js";
7
7
  function toErrorMessage(error) {
8
8
  return error instanceof Error ? error.message : String(error);
9
9
  }
@@ -307,7 +307,7 @@ export async function bootstrapCreateCampaign(input = {}) {
307
307
  ? resumeDetected
308
308
  ? `Bootstrap complete.${workspaceNotice}${modelNotice} 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.`
309
309
  : flowVersion === "v2"
310
- ? `Bootstrap complete.${workspaceNotice}${modelNotice} Load the compact create-campaign-v2 entry prompt once with get_subskill_prompt({ subskillName: "create-campaign-v2" }); Hermes users should start this flow with /sellable-create-campaign. Load flow/reference assets lazily only when that stage needs them. Preserve the pre-intake sequence: confirm auth/workspace status, ask only for the LinkedIn profile URL or handle, normalize handles to a full profile URL, require that profile identity before continuing, run lightweight profile/company lookup, then ask the target, offer, credibility, and prospect-source setup questions. Do not call list_senders or sender discovery during setup; sender availability belongs only to Settings after message approval. Then write the campaign brief, call create_campaign once to mint the watchable shell, surface the returned watch link once before brief approval, and hand off to lead finding without repeating the link.`
310
+ ? `Bootstrap complete.${workspaceNotice}${modelNotice} Load the compact create-campaign-v2 entry prompt once with get_subskill_prompt({ subskillName: "create-campaign-v2" }); Hermes users should start this flow with /sellable-create-campaign. Load flow/reference assets lazily only when that stage needs them. Preserve the pre-intake sequence: confirm auth/workspace status, ask only for the LinkedIn profile URL or handle, normalize handles to a full profile URL, require that profile identity before continuing, run lightweight profile/company lookup, then ask the target, offer, credibility, and prospect-source setup questions. Do not call list_senders or sender discovery during setup; sender availability belongs only to Settings after message approval. Then write the campaign brief, call create_campaign once to mint the watchable shell, surface the returned watch link once before brief approval, and hand off to lead finding without repeating the link. Exception: when you entered from the first-campaign-onboarding skill, its first-campaign rules replace the workspace notice, the slash-command note, the setup questions, and the watch-link handoff described here.`
311
311
  : `Bootstrap complete.${workspaceNotice}${modelNotice} 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.`
312
312
  : "Bootstrap incomplete. Resolve blockingErrors and rerun bootstrap_create_campaign before provider/search/import tools.";
313
313
  // Strip prompt body from createCampaignSubskill — it's loaded via the host
@@ -619,6 +619,7 @@ export declare const campaignToolDefinitions: ({
619
619
  };
620
620
  safety: {
621
621
  type: string[];
622
+ description: string;
622
623
  };
623
624
  progressLabel: {
624
625
  type: string[];
@@ -859,6 +860,7 @@ export declare const campaignToolDefinitions: ({
859
860
  };
860
861
  safety: {
861
862
  type: string[];
863
+ description: string;
862
864
  };
863
865
  progressLabel: {
864
866
  type: string[];
@@ -29,7 +29,10 @@ const WATCH_NARRATION_TOOL_SCHEMA = {
29
29
  visibleState: { type: "string" },
30
30
  agentIntent: { type: "string" },
31
31
  nextAction: { type: ["string", "null"] },
32
- safety: { type: ["string", "null"] },
32
+ safety: {
33
+ type: ["string", "null"],
34
+ description: 'Required whenever currentStep changes: one short line on what will not happen yet (for example "Rules only. No one is contacted."). Optional otherwise.',
35
+ },
33
36
  progressLabel: { type: ["string", "null"] },
34
37
  blockedReason: { type: ["string", "null"] },
35
38
  workerStatuses: {
@@ -2702,6 +2702,7 @@ export declare const allTools: ({
2702
2702
  };
2703
2703
  safety: {
2704
2704
  type: string[];
2705
+ description: string;
2705
2706
  };
2706
2707
  progressLabel: {
2707
2708
  type: string[];
@@ -2942,6 +2943,7 @@ export declare const allTools: ({
2942
2943
  };
2943
2944
  safety: {
2944
2945
  type: string[];
2946
+ description: string;
2945
2947
  };
2946
2948
  progressLabel: {
2947
2949
  type: string[];
@@ -7486,6 +7488,8 @@ export declare const allTools: ({
7486
7488
  expectedCalendarLink?: undefined;
7487
7489
  expectedCalendarLinkRevision?: undefined;
7488
7490
  force?: undefined;
7491
+ senderContext?: undefined;
7492
+ expectedUpdatedAt?: undefined;
7489
7493
  };
7490
7494
  required: string[];
7491
7495
  additionalProperties: boolean;
@@ -7527,6 +7531,8 @@ export declare const allTools: ({
7527
7531
  threshold?: undefined;
7528
7532
  maxStalenessSeconds?: undefined;
7529
7533
  paidInmailCreditsMaxStalenessSeconds?: undefined;
7534
+ senderContext?: undefined;
7535
+ expectedUpdatedAt?: undefined;
7530
7536
  };
7531
7537
  required: string[];
7532
7538
  additionalProperties: boolean;
@@ -7610,6 +7616,89 @@ export declare const allTools: ({
7610
7616
  idempotentHint: boolean;
7611
7617
  openWorldHint: boolean;
7612
7618
  };
7619
+ } | {
7620
+ name: string;
7621
+ description: string;
7622
+ inputSchema: {
7623
+ type: string;
7624
+ properties: {
7625
+ senderId: {
7626
+ type: string;
7627
+ minLength: number;
7628
+ description?: undefined;
7629
+ };
7630
+ workspaceId: {
7631
+ type: string;
7632
+ minLength: number;
7633
+ description?: undefined;
7634
+ };
7635
+ senderContext: {
7636
+ type: string[];
7637
+ properties: {
7638
+ workStories: {
7639
+ readonly type: "array";
7640
+ readonly maxItems: 10;
7641
+ readonly items: {
7642
+ readonly type: "string";
7643
+ readonly minLength: 1;
7644
+ readonly maxLength: 3000;
7645
+ };
7646
+ };
7647
+ personalNotes: {
7648
+ readonly type: "array";
7649
+ readonly maxItems: 10;
7650
+ readonly items: {
7651
+ readonly type: "string";
7652
+ readonly minLength: 1;
7653
+ readonly maxLength: 3000;
7654
+ };
7655
+ };
7656
+ voiceExamples: {
7657
+ readonly type: "array";
7658
+ readonly maxItems: 10;
7659
+ readonly items: {
7660
+ readonly type: "string";
7661
+ readonly minLength: 1;
7662
+ readonly maxLength: 3000;
7663
+ };
7664
+ };
7665
+ sourcesAndLimits: {
7666
+ readonly type: "array";
7667
+ readonly maxItems: 10;
7668
+ readonly items: {
7669
+ readonly type: "string";
7670
+ readonly minLength: 1;
7671
+ readonly maxLength: 3000;
7672
+ };
7673
+ };
7674
+ };
7675
+ additionalProperties: boolean;
7676
+ };
7677
+ expectedUpdatedAt: {
7678
+ readonly type: readonly ["string", "null"];
7679
+ };
7680
+ actionType?: undefined;
7681
+ campaignId?: undefined;
7682
+ tableId?: undefined;
7683
+ columnId?: undefined;
7684
+ threshold?: undefined;
7685
+ maxStalenessSeconds?: undefined;
7686
+ paidInmailCreditsMaxStalenessSeconds?: undefined;
7687
+ calendarLink?: undefined;
7688
+ expectedCalendarLink?: undefined;
7689
+ expectedCalendarLinkRevision?: undefined;
7690
+ force?: undefined;
7691
+ };
7692
+ required: string[];
7693
+ additionalProperties: boolean;
7694
+ };
7695
+ annotations: {
7696
+ readOnlyHint: boolean;
7697
+ destructiveHint: boolean;
7698
+ idempotentHint: boolean;
7699
+ openWorldHint: boolean;
7700
+ };
7701
+ outputSchema?: undefined;
7613
7702
  } | {
7614
7703
  name: string;
7615
7704
  description: string;
@@ -180,6 +180,8 @@ export declare const senderToolDefinitions: ({
180
180
  expectedCalendarLink?: undefined;
181
181
  expectedCalendarLinkRevision?: undefined;
182
182
  force?: undefined;
183
+ senderContext?: undefined;
184
+ expectedUpdatedAt?: undefined;
183
185
  };
184
186
  required: never[];
185
187
  additionalProperties: boolean;
@@ -213,6 +215,8 @@ export declare const senderToolDefinitions: ({
213
215
  expectedCalendarLink?: undefined;
214
216
  expectedCalendarLinkRevision?: undefined;
215
217
  force?: undefined;
218
+ senderContext?: undefined;
219
+ expectedUpdatedAt?: undefined;
216
220
  };
217
221
  required: string[];
218
222
  additionalProperties: boolean;
@@ -267,6 +271,8 @@ export declare const senderToolDefinitions: ({
267
271
  expectedCalendarLink?: undefined;
268
272
  expectedCalendarLinkRevision?: undefined;
269
273
  force?: undefined;
274
+ senderContext?: undefined;
275
+ expectedUpdatedAt?: undefined;
270
276
  };
271
277
  required: string[];
272
278
  additionalProperties: boolean;
@@ -308,6 +314,8 @@ export declare const senderToolDefinitions: ({
308
314
  threshold?: undefined;
309
315
  maxStalenessSeconds?: undefined;
310
316
  paidInmailCreditsMaxStalenessSeconds?: undefined;
317
+ senderContext?: undefined;
318
+ expectedUpdatedAt?: undefined;
311
319
  };
312
320
  required: string[];
313
321
  additionalProperties: boolean;
@@ -391,9 +399,108 @@ export declare const senderToolDefinitions: ({
391
399
  idempotentHint: boolean;
392
400
  openWorldHint: boolean;
393
401
  };
402
+ } | {
403
+ name: string;
404
+ description: string;
405
+ inputSchema: {
406
+ type: string;
407
+ properties: {
408
+ senderId: {
409
+ type: string;
410
+ minLength: number;
411
+ description?: undefined;
412
+ };
413
+ workspaceId: {
414
+ type: string;
415
+ minLength: number;
416
+ description?: undefined;
417
+ };
418
+ senderContext: {
419
+ type: string[];
420
+ properties: {
421
+ workStories: {
422
+ readonly type: "array";
423
+ readonly maxItems: 10;
424
+ readonly items: {
425
+ readonly type: "string";
426
+ readonly minLength: 1;
427
+ readonly maxLength: 3000;
428
+ };
429
+ };
430
+ personalNotes: {
431
+ readonly type: "array";
432
+ readonly maxItems: 10;
433
+ readonly items: {
434
+ readonly type: "string";
435
+ readonly minLength: 1;
436
+ readonly maxLength: 3000;
437
+ };
438
+ };
439
+ voiceExamples: {
440
+ readonly type: "array";
441
+ readonly maxItems: 10;
442
+ readonly items: {
443
+ readonly type: "string";
444
+ readonly minLength: 1;
445
+ readonly maxLength: 3000;
446
+ };
447
+ };
448
+ sourcesAndLimits: {
449
+ readonly type: "array";
450
+ readonly maxItems: 10;
451
+ readonly items: {
452
+ readonly type: "string";
453
+ readonly minLength: 1;
454
+ readonly maxLength: 3000;
455
+ };
456
+ };
457
+ };
458
+ additionalProperties: boolean;
459
+ };
460
+ expectedUpdatedAt: {
461
+ readonly type: readonly ["string", "null"];
462
+ };
463
+ actionType?: undefined;
464
+ campaignId?: undefined;
465
+ tableId?: undefined;
466
+ columnId?: undefined;
467
+ threshold?: undefined;
468
+ maxStalenessSeconds?: undefined;
469
+ paidInmailCreditsMaxStalenessSeconds?: undefined;
470
+ calendarLink?: undefined;
471
+ expectedCalendarLink?: undefined;
472
+ expectedCalendarLinkRevision?: undefined;
473
+ force?: undefined;
474
+ };
475
+ required: string[];
476
+ additionalProperties: boolean;
477
+ };
478
+ annotations: {
479
+ readOnlyHint: boolean;
480
+ destructiveHint: boolean;
481
+ idempotentHint: boolean;
482
+ openWorldHint: boolean;
483
+ };
484
+ outputSchema?: undefined;
394
485
  })[];
395
486
  export declare function listSenders(input?: ListSendersInput): Promise<ListSendersResponse>;
396
487
  export declare function setSenderCalendarLink(input: SetSenderCalendarLinkInput): Promise<SenderCalendarLinkToolResult>;
488
+ declare const SENDER_CONTEXT_LISTS: readonly ["workStories", "personalNotes", "voiceExamples", "sourcesAndLimits"];
489
+ export type SetSenderContextInput = {
490
+ senderId: string;
491
+ workspaceId: string;
492
+ senderContext: Partial<Record<(typeof SENDER_CONTEXT_LISTS)[number], string[]>> | null;
493
+ expectedUpdatedAt: string | null;
494
+ };
495
+ /** Save one sender's personal context through the compare-before-write route. */
496
+ export declare function setSenderContext(input: SetSenderContextInput): Promise<{
497
+ isError?: true | undefined;
498
+ structuredContent: Record<string, unknown>;
499
+ content: {
500
+ type: "text";
501
+ text: string;
502
+ }[];
503
+ }>;
397
504
  export declare function getSender(input: GetSenderInput): Promise<SenderDetailResponse>;
398
505
  export declare function refreshPaidInmailCredits(input: RefreshPaidInmailCreditsInput): Promise<RefreshPaidInmailCreditsResponse>;
399
506
  export {};
@@ -2,6 +2,11 @@ import { getApi, SellableApiError } from "../api.js";
2
2
  import { isLinkedInProfileInput, normalizeLinkedInProfileInput, } from "./linkedin-url.js";
3
3
  import { normalizeExplicitWorkspaceId, workspaceRequestOptions, } from "./workspace-context.js";
4
4
  const nullableStringSchema = { type: ["string", "null"] };
5
+ const senderContextEntriesInputSchema = {
6
+ type: "array",
7
+ maxItems: 10,
8
+ items: { type: "string", minLength: 1, maxLength: 3000 },
9
+ };
5
10
  const setSenderCalendarLinkOutputSchema = {
6
11
  type: "object",
7
12
  oneOf: [
@@ -346,6 +351,41 @@ export const senderToolDefinitions = [
346
351
  openWorldHint: false,
347
352
  },
348
353
  },
354
+ {
355
+ name: "set_sender_context",
356
+ description: "Save one exact outbound sender's personal context: the four lists the message writer uses as that sender's fact bank and voice (workStories, personalNotes, voiceExamples, sourcesAndLimits). This is what a sender interview produces. It replaces the sender's whole saved context; it cannot touch another sender, the calendar link, writing style, dos/donts or Company Info. Read get_sender first and pass generationContext.updatedAt as expectedUpdatedAt (null when nothing is saved yet). Limits: at most 10 entries per list, 3000 characters per entry, 15000 UTF-8 bytes in all. Only write what the sender said or confirmed; never copy one sender's facts into another's. Pass senderContext: null to clear it.",
357
+ inputSchema: {
358
+ type: "object",
359
+ properties: {
360
+ senderId: { type: "string", minLength: 1 },
361
+ workspaceId: { type: "string", minLength: 1 },
362
+ senderContext: {
363
+ type: ["object", "null"],
364
+ properties: {
365
+ workStories: senderContextEntriesInputSchema,
366
+ personalNotes: senderContextEntriesInputSchema,
367
+ voiceExamples: senderContextEntriesInputSchema,
368
+ sourcesAndLimits: senderContextEntriesInputSchema,
369
+ },
370
+ additionalProperties: false,
371
+ },
372
+ expectedUpdatedAt: nullableStringSchema,
373
+ },
374
+ required: [
375
+ "senderId",
376
+ "workspaceId",
377
+ "senderContext",
378
+ "expectedUpdatedAt",
379
+ ],
380
+ additionalProperties: false,
381
+ },
382
+ annotations: {
383
+ readOnlyHint: false,
384
+ destructiveHint: true,
385
+ idempotentHint: true,
386
+ openWorldHint: false,
387
+ },
388
+ },
349
389
  ];
350
390
  export async function listSenders(input = {}) {
351
391
  const api = getApi();
@@ -497,6 +537,134 @@ export async function setSenderCalendarLink(input) {
497
537
  });
498
538
  }
499
539
  }
540
+ const SENDER_CONTEXT_LISTS = [
541
+ "workStories",
542
+ "personalNotes",
543
+ "voiceExamples",
544
+ "sourcesAndLimits",
545
+ ];
546
+ function senderContextResult(value) {
547
+ return {
548
+ structuredContent: value,
549
+ content: [{ type: "text", text: JSON.stringify(value) }],
550
+ ...(value.ok === true ? {} : { isError: true }),
551
+ };
552
+ }
553
+ function isSenderContextInput(value) {
554
+ if (value === null)
555
+ return true;
556
+ if (!value || typeof value !== "object" || Array.isArray(value))
557
+ return false;
558
+ return Object.entries(value).every(([key, entries]) => SENDER_CONTEXT_LISTS.includes(key) &&
559
+ Array.isArray(entries) &&
560
+ entries.every((entry) => typeof entry === "string"));
561
+ }
562
+ /** Save one sender's personal context through the compare-before-write route. */
563
+ export async function setSenderContext(input) {
564
+ const candidate = input;
565
+ const record = candidate && typeof candidate === "object" && !Array.isArray(candidate)
566
+ ? candidate
567
+ : null;
568
+ const senderId = record && typeof record.senderId === "string" ? record.senderId.trim() : "";
569
+ const workspaceId = record && typeof record.workspaceId === "string"
570
+ ? record.workspaceId.trim()
571
+ : "";
572
+ const hasOwn = (key) => record !== null && Object.prototype.hasOwnProperty.call(record, key);
573
+ if (!record ||
574
+ !senderId ||
575
+ !workspaceId ||
576
+ !hasOwn("senderContext") ||
577
+ !isSenderContextInput(record.senderContext) ||
578
+ !hasOwn("expectedUpdatedAt") ||
579
+ !isNullableString(record.expectedUpdatedAt)) {
580
+ return senderContextResult({
581
+ ok: false,
582
+ code: "invalid_arguments",
583
+ message: "senderId, workspaceId, senderContext and expectedUpdatedAt are required with the documented types.",
584
+ status: null,
585
+ uncertain: false,
586
+ currentUpdatedAt: null,
587
+ guidance: "Read get_sender and retry with generationContext.updatedAt as expectedUpdatedAt (null when nothing is saved).",
588
+ });
589
+ }
590
+ const api = getApi();
591
+ try {
592
+ const receipt = await api.put(`/api/v3/mcp/senders/${encodeURIComponent(senderId)}/sender-context`, {
593
+ workspaceId,
594
+ senderContext: record.senderContext,
595
+ expectedUpdatedAt: record.expectedUpdatedAt,
596
+ }, workspaceRequestOptions(workspaceId));
597
+ if (!receipt ||
598
+ typeof receipt !== "object" ||
599
+ typeof receipt.updatedAt !== "string") {
600
+ return senderContextResult({
601
+ ok: false,
602
+ code: "malformed_sender_context_response",
603
+ message: "The sender-context save returned an incomplete receipt, so the final state is uncertain.",
604
+ status: null,
605
+ uncertain: true,
606
+ currentUpdatedAt: null,
607
+ guidance: "Reread get_sender and reconcile before retrying.",
608
+ });
609
+ }
610
+ return senderContextResult({
611
+ ok: true,
612
+ senderId,
613
+ workspaceId,
614
+ senderContext: receipt.senderContext ?? null,
615
+ previousUpdatedAt: record.expectedUpdatedAt,
616
+ updatedAt: receipt.updatedAt,
617
+ cleared: receipt.cleared === true,
618
+ });
619
+ }
620
+ catch (error) {
621
+ if (error instanceof SellableApiError) {
622
+ const body = parseErrorBody(error.body);
623
+ const code = safeErrorCode(body?.code, `http_${error.status}`);
624
+ const isConflict = error.status === 409 && code === "sender_context_conflict";
625
+ const uncertain = error.status >= 500;
626
+ return senderContextResult({
627
+ ok: false,
628
+ code,
629
+ message: isConflict
630
+ ? "The sender's saved context changed after it was read."
631
+ : uncertain
632
+ ? "The sender-context save did not complete with a confirmed result."
633
+ : error.isAuthError
634
+ ? "Sellable authentication or authorization failed."
635
+ : error.status === 404
636
+ ? "The sender was not found in the explicit workspace."
637
+ : "The sender context was not saved.",
638
+ status: error.status,
639
+ uncertain,
640
+ currentUpdatedAt: isConflict && isNullableString(body?.currentUpdatedAt)
641
+ ? body.currentUpdatedAt
642
+ : null,
643
+ guidance: isConflict
644
+ ? "Reread get_sender, merge with the newer context, and retry with its updatedAt."
645
+ : uncertain
646
+ ? "Reread get_sender and reconcile before retrying."
647
+ : error.isAuthError
648
+ ? error.guidance ||
649
+ "Authenticate again and retry the same scoped request."
650
+ : error.status === 400
651
+ ? "Keep each list to 10 entries of at most 3000 characters and 15000 UTF-8 bytes in all, then retry."
652
+ : error.status === 404
653
+ ? "Reread list_senders in the explicit workspace and select an existing sender."
654
+ : null,
655
+ });
656
+ }
657
+ return senderContextResult({
658
+ ok: false,
659
+ code: "sender_context_uncertain",
660
+ message: "The sender-context request ended without a confirmed response, so the final state is uncertain.",
661
+ status: null,
662
+ uncertain: true,
663
+ currentUpdatedAt: null,
664
+ guidance: "Reread get_sender and reconcile before retrying.",
665
+ });
666
+ }
667
+ }
500
668
  export async function getSender(input) {
501
669
  const api = getApi();
502
670
  const requestOptions = workspaceRequestOptions(input.workspaceId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.950",
3
+ "version": "0.1.952",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code, Codex, and Hermes campaign workflows",
6
6
  "main": "dist/index.js",
@@ -53,6 +53,13 @@ The bracket is replaced at generation time by either (a) the rendered
53
53
  sentence, or (b) nothing (omit). The brackets themselves never appear in
54
54
  the final message.
55
55
 
56
+ **When you save the template in the campaign brief, write each bracket on one
57
+ line.** The examples below wrap a bracket across lines so it is readable here.
58
+ The brief validator reads a bracket that spans three or more lines as message
59
+ prose and rejects the save ("3+ consecutive non-empty prose lines with only
60
+ single newlines"). Keep the DO / DON'T / FALLBACK labels; drop the line breaks
61
+ inside the bracket.
62
+
56
63
  ## Required clauses inside an AI-native token
57
64
 
58
65
  Every well-specified AI-native token must include four things:
@@ -90,5 +90,8 @@ messages that pass my quality check and launch for you, then keep your sends fil
90
90
  Reply review first if you would rather approve." Save the answer with `customer_program`
91
91
  `save_context` as `launchConsent: { mode: "autopilot" | "review_first", decidedAt }`.
92
92
  A yes, or no objection, is `autopilot`; a wish to approve first is `review_first`.
93
- Never ask again; a later "review first" switches to `review_first`, and "pause" sets the saved work and sending pauses with `set_preferences`. If the
93
+ A reply that answers other questions and says nothing about this offer is not an answer to it:
94
+ save nothing and keep every approval. Never ask again; a later "review first" switches to `review_first`, and "pause" sets the saved work and sending pauses with `set_preferences`. If the
94
95
  customer has not answered when you reach launch, keep the launch confirmation.
96
+ During a new workspace's first campaign (`first-campaign-onboarding`) do not make this offer at all:
97
+ the first launch always needs the customer's explicit yes.
@@ -40,7 +40,8 @@ states.
40
40
  sample you are checking, and why that helps this campaign.
41
41
  - At filter choice, do not say the batch is filtering before rubrics are saved.
42
42
  - Avoid internal terms: MCP, tool, currentStep, workflow table, scout, debug.
43
- - Treat `safety` as optional internal context, not a visible disclaimer.
43
+ - Always send a short `safety` line when the step changes; the API rejects a
44
+ step change without one. It is internal context, not a visible disclaimer.
44
45
  - Do not repeat long negative lists like "no leads import, no enrichment, no
45
46
  messages, no sequence, no sending" in routine watch copy. Prefer one concise
46
47
  gate phrase only when it matters: "This only approves me to look for the best
@@ -198,9 +198,8 @@ import milestone.
198
198
  `messaging.critique.enabled`, `handoff.autoStart`,
199
199
  `handoff.orientation`, `retry.sameToolSameError`,
200
200
  `logging.logEveryThresholdTrip`.
201
- 2. Resolve and materialize the approved source after Start Import approval. Load
202
- `references/post-mint-source-materialization.md` when the source is normal
203
- discovery or when a legacy no-shell approval fixture must be replayed. If
201
+ 2. Resolve and materialize the approved source after Start Import approval.
202
+ `references/step-13-import-leads.md` holds the import steps. If
204
203
  CampaignOffer state already attached searches/selections to this campaign,
205
204
  reuse them. If there is no `lead-source-intake.json`, replay the approved
206
205
  provider recipe from `lead-review.md` with `campaignOfferId` before
@@ -622,7 +621,6 @@ runs.
622
621
  | ------------------------------------------------ | ------------------------------------------------------------------------- |
623
622
  | `references/approval-gate-framing.md` | Approval gate, before showing the approval packet |
624
623
  | `references/watch-link-handoff.md` | First brief handoff + explicit link recovery only |
625
- | `references/post-mint-source-materialization.md` | Step 13, before importing normal-discovery or legacy campaignless sources |
626
624
  | `references/sample-validation-loop.md` | Step 14, before enriching + scoring the sample |
627
625
  | `references/escalation-ladder.md` | Any tail step that needs to decide retry / revise / escalate |
628
626
  | `references/final-handoff-contract.md` | Step 16, and every Claude greenlight turn |
@@ -48,11 +48,14 @@ reply to either the same way.)
48
48
  A reply in that thread is explicit permission to begin preparation, not
49
49
  permission to send. It needs no mention of this Agent.
50
50
 
51
- - **The reply contains a LinkedIn profile URL.** Before any tool call, send
52
- one short line so the customer is not left looking at silence: "Got it.
53
- Researching you and your company now. I'll be back in a couple of minutes
54
- with a first campaign to review." Then enter **Create-Campaign Handoff** in
55
- the same turn, with that URL as the sender identity.
51
+ - **The reply contains a LinkedIn profile URL.** Send one short line first so
52
+ the customer is not left looking at silence: "Got it. Researching you and
53
+ your company now. I'll be back in a few minutes with a first campaign to
54
+ review." Send it before the Live State Check and before loading any other
55
+ skill or reference; those reads take over a minute. Then run the Live State
56
+ Check and enter **Create-Campaign Handoff** in the same turn, with that URL
57
+ as the sender identity. If the Live State Check finds a campaign already
58
+ launched, say so in the next line and offer to help with that campaign.
56
59
  - **The reply is a yes, or anything else that agrees, with no profile URL.**
57
60
  Ask for it once, in one sentence: "What's your LinkedIn profile URL? I'll
58
61
  use it to research you and your company." Ask nothing else.
@@ -209,18 +212,220 @@ continue in the same conversation; do not ask the user to run another command.
209
212
 
210
213
  1. Call `bootstrap_create_campaign({ flowVersion:"v2", host:<current host> })`.
211
214
  2. Load `get_subskill_prompt({ subskillName:"create-campaign-v2" })` to
212
- `hasMore:false` and load every required asset it names.
215
+ `hasMore:false`, then `core/flow.v2.json`. Load every other reference only
216
+ at the stage that needs it. The customer is waiting in silence, so make
217
+ these reads in as few turns as you can: the Live State Check reads together
218
+ in one turn; then bootstrap, the create-campaign-v2 prompt, the flow file,
219
+ `research-sender` and `list_dnc_entries({ limit:1 })` together in the next.
213
220
  3. Reuse known identity, company, audience, offer, and any explicit DNC decision.
214
- Ask only missing intake in the canonical flow. A public LinkedIn profile URL
215
- for company research is not a connected sending account: the latter belongs
216
- at Settings, not before the draft. Preserve the normal brief/watch, source,
217
- filter, message, sender, sequence, and approval gates. Offer CRM or meeting
218
- integrations only as optional context when helpful.
221
+ A public LinkedIn profile URL for company research is not a connected
222
+ sending account: the latter belongs at Settings, not before the draft.
223
+ The flow's order and state gates still hold: the campaign exists before
224
+ anyone is imported, the source is chosen before the import, sample rows
225
+ exist before filters and messages run, and the template is saved before
226
+ anything is queued. Who decides each one is set by the next section. Offer
227
+ CRM or meeting integrations only as optional context when helpful.
219
228
  4. Reuse an existing draft for the same goal. Never create a duplicate because
220
229
  onboarding resumed in a new session.
221
230
  5. Agreement authorizes campaign creation/preparation only, not launch. Never
222
- call `start_campaign` until the create-campaign workflow presents its final
223
- launch packet and the user explicitly approves Start.
231
+ call `start_campaign` until the customer has seen who sends and how many a
232
+ day, and explicitly says to launch.
233
+
234
+ ### The first campaign in three approvals
235
+
236
+ The welcome promised "a first campaign for you to review". For a new
237
+ workspace's first campaign the customer is asked for three things and nothing
238
+ else: the **direction**, the **sample**, and the **launch**. You decide
239
+ everything in between yourself, using the flow's own hands-off rules
240
+ (`yoloMode.autoSelectsPreLaunchChoices`, except its "brief approval" and
241
+ "generated message review" entries, which are approvals 1 and 2 here), and say
242
+ what you chose in one plain line when you report back. Where the general workflow, a reference file, or a
243
+ tool result says otherwise, this section wins for the first campaign.
244
+
245
+ This is not the standing `launchConsent` autopilot. Do not ask about or save
246
+ `launchConsent` during the first campaign, and never skip one of the three
247
+ approvals. Silence, or a reply that answers something else, is never an
248
+ approval. The daily review offers hands-off sending once the first campaign is
249
+ running.
250
+
251
+ **What you never ask.** Not the four setup questions ("Who should we target
252
+ first?", "What should we pitch?", the credibility question, "How should we
253
+ find prospects?"): giving the profile URL is the customer's instruction to use
254
+ Sellable's researched recommendation and to find prospects for them, which is
255
+ the flow's own skip rule for each question. Not "is this the right company?"
256
+ or "which of your offers should I lead with?": pick the one the profile and
257
+ site lead with and say so in the direction, where they can correct it. Not a
258
+ separate approval for where
259
+ to look, for starting the import, for using filters, for the filter rules, or
260
+ for the message template: those are yours to decide and report.
261
+
262
+ **How you write in Slack.**
263
+
264
+ - Ask in plain words the customer can answer with yes or with the change they
265
+ want. Slack has no question panel: never ask for option letters, and never
266
+ mention a question panel or a config file.
267
+ - Post nothing between "Got it" and the direction: no research progress lines,
268
+ tool names, counts of proof items, or which workspace you are in (this Agent
269
+ is locked to one workspace and the customer knows which).
270
+ - Never print the "WATCH … BUILD THE CAMPAIGN LIVE" block, "Keep this chat
271
+ open", "Brief approved", or any line that tells the customer to run a
272
+ command. Those are written for other hosts.
273
+ - The channel is shared, and the returned `watchUrl` carries a sign-in token.
274
+ Post that URL with only its `token` parameter removed, keeping the rest
275
+ exactly as returned. The customer signed in to Sellable when they set this
276
+ Agent up. This replaces the tool's instruction to print the handoff block
277
+ and its rule against changing the link.
278
+ - Do not paste the full brief into Slack. Save it on the campaign and post the
279
+ five-line summary below.
280
+
281
+ **Approval 1: direction.** After research, write the brief, create the
282
+ campaign with it (`create_campaign`), and post one message in this shape. The
283
+ saved brief is the full brief; this message is how the customer approves it,
284
+ so skip the flow's setup receipt, its "Does this brief look right…" question
285
+ and the inline brief.
286
+
287
+ ```text
288
+ Here's the first campaign I'd run for {company}.
289
+
290
+ *Who* {the buyer, one line}
291
+ *Why now* {why these people care right now, one line}
292
+ *Offer* {what you would get to once they reply, and the small ask, one line}
293
+ *Proof* {the proof you would lead with; if you found none, say so and what you would use instead}
294
+ *Where I'll look* {where you will find them first, in plain words}
295
+
296
+ Full brief: {campaign link}
297
+
298
+ Two things from you:
299
+ 1. Does this look right? Reply yes, or tell me what to change.
300
+ 2. Is there anyone I must never contact: customers, open deals, people who opted out? Paste the list here, or say "no list".
301
+
302
+ Nothing is sent on LinkedIn until you approve the launch.
303
+ ```
304
+
305
+ The first message to a prospect opens a conversation; the message rules
306
+ decide its wording, and they keep the pitch and the ask out of it. So write
307
+ the *Offer* line as what comes once the prospect replies, and do not promise,
308
+ here or in the saved brief, that the first message will mention the post
309
+ someone engaged with or carry the ask.
310
+
311
+ If something you assumed could be stale (for example the profile pitches two
312
+ different things), say which one you picked in the line it affects. Leave out
313
+ the second question, and write "One thing from you:", when
314
+ `list_dnc_entries` already shows a list or the saved program context has
315
+ `dncState` `provided` or `explicit_none`. A yes, or "keep going", with no
316
+ answer to the second question is not a "no list" decision, and it is not a
317
+ reason to stop either: carry on with the list as it is, add to your
318
+ acknowledgement "You can send me a do-not-contact list any time before
319
+ launch.", and ask once more in the launch message. Never hold the work
320
+ waiting for a list. Follow the DNC Contract for a pasted list, and save
321
+ the decision with `customer_program` `save_context` (`dncState:
322
+ "explicit_none"` for "no list", `"provided"` once a list is imported) so a
323
+ later session does not ask again. The answer given here settles the flow's
324
+ do-not-contact gate; do not ask again before the import.
325
+
326
+ **Between 1 and 2.** Once the direction and the do-not-contact answer are in,
327
+ reply first, before any tool call: "On it. I'll find the people, check who
328
+ fits and write the message. Your sample will be here in about 15 to 20
329
+ minutes." Then set `interactionMode: "autonomous"` on the campaign with
330
+ `update_campaign` and pass `confirmed: true` where a search or import tool
331
+ asks for it: the direction approval is that confirmation. Do the work without
332
+ asking: pick where to look, find and import the people within the flow's
333
+ caps, use filters and write the rules, write the message, and prepare the
334
+ 15-person sample.
335
+
336
+ - **Keep the customer posted.** Whenever about five minutes have passed since
337
+ your last message, post one short line in plain words on where you are
338
+ ("Found 370 people who engaged with three posts about this. Checking who
339
+ fits and writing the message now."). No more often than that.
340
+ - **The live builder.** Where the flow's required watch copy tells the
341
+ customer to approve something this section removed, write instead what is
342
+ happening and that nothing is needed from them yet.
343
+ - **Import size.** If `import_leads` says the selected posts cover fewer
344
+ people than the target, select again with the number it says they cover and
345
+ retry once. Do not start a new search for a small gap.
346
+ - **Waits.** A wait tool that stops with `tool_timeout_guard` has only reached
347
+ the host's time limit. Call it again while work is still running.
348
+ - **A sample where nobody fits is not a pause.** Follow the flow's own
349
+ recovery first: a fresh sample from the same list. Stop early only for the
350
+ flow's must-pause reasons: missing data, or a source, filter or message
351
+ that still fails its quality floor after that recovery. Then say what
352
+ failed and what you suggest, as one question.
353
+ - **Let the sample finish.** The flow stops at the first passing message; here
354
+ wait until the 15-person sample has finished scoring and writing, because
355
+ the customer is shown three people and a count.
356
+ - **Check before you show.** For each message you are about to show, compare
357
+ the company it names with the person's own headline. Leave out any that
358
+ disagree and say that you dropped them.
359
+
360
+ **Approval 2: sample.** When the sample is ready, post real people and the
361
+ real message each would receive:
362
+
363
+ ```text
364
+ Your sample is ready: {n} real people and the message each would get.
365
+
366
+ {Name} · {title}, {company} · {why they are here, a few words}
367
+ "{the message they would receive}"
368
+
369
+ {two more like this}
370
+
371
+ {passed} of {checked} people I checked fit. {one line on why the rest did not}
372
+ What I chose: {where the people came from and how many are on the list, and the fit rules in a few words}
373
+ The first message has no pitch on purpose. {the offer} comes once they reply.
374
+ Everyone and every message: {campaign link}
375
+
376
+ Reply approve to use these people and this message, or tell me what to change.
377
+ Nothing has been sent.
378
+ ```
379
+
380
+ **After "approve".** Reply first: "Approved." plus anything they changed, in
381
+ the same line ("Approved, without Corey."). Mark every person in the sample
382
+ the customer kept as approved in the table, not one or two. A person the
383
+ customer dropped stays unapproved; never approve them later, in bulk or
384
+ otherwise.
385
+
386
+ **Approval 3: launch.** The only thing left is the sending account. Call
387
+ `list_senders`.
388
+
389
+ - **No usable sender.** Ask for that one thing: "One thing left: connect the
390
+ LinkedIn account that will send. https://app.sellable.dev/linkedin-accounts
391
+ It takes about two minutes, and it is only used to send what you approve;
392
+ finding and checking people happened outside your account. Tell me when
393
+ it's done." When they say it is done, call `list_senders` again. If nothing
394
+ usable is there yet, say so plainly and give the link again.
395
+ - **One connected sender that is the person you researched.** Use it without
396
+ asking. If the connected account is someone else, or more than one is
397
+ connected, ask which account should send. That is the one extra question
398
+ this section allows.
399
+ - **Get it ready, then ask.** Attach the sender and the recommended sequence
400
+ to the campaign yourself (`update_campaign` with `senderIds`, then
401
+ `attach_recommended_sequence`); this prepares the launch and sends nothing.
402
+ Read the real numbers for that sender (`refill_v3_world_state` for the next
403
+ two days). Then post:
404
+
405
+ ```text
406
+ Ready to launch "{campaign name}".
407
+
408
+ *Sender* {name}
409
+ *First sends* {n} approved messages, {how many a day and which days, from the numbers you read}
410
+ *Replies* I'll post each reply here as a draft for you to approve.
411
+ {only if no do-not-contact answer was ever given: "You haven't sent a do-not-contact list. Paste one now if there is anyone I must not message."}
412
+
413
+ Reply launch to start. You can pause it any time by telling me.
414
+ ```
415
+
416
+ State only numbers you read. If you could not read one, leave that part out
417
+ rather than guess.
418
+ - **Start only on an explicit yes** ("launch", "yes, start", "go"). Then call
419
+ `start_campaign` and report what happened: what is scheduled and for when,
420
+ and that scheduled is not sent yet.
421
+ - **After the launch, one question about the rest.** Only the sample has been
422
+ checked and written. Ask: "{remaining} more people are on the list. Want me
423
+ to keep going through them and approve the messages that pass the same
424
+ checks, so your sends stay filled? Reply yes, or review first." Save the
425
+ answer as the standing `launchConsent` (`customer_program` `save_context`:
426
+ `autopilot` for yes, `review_first` otherwise). This is the first and only
427
+ time that question is asked during onboarding, and only an explicit yes is
428
+ `autopilot`.
224
429
 
225
430
  ## Hard Boundaries
226
431
 
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: research-sender
3
- description: "Parallel-first sender research protocol. One round of five batched tool calls (fetch_linkedin_profile + fetch_company + 3× WebSearch), no enrich_sender required, ~30-40s wall time."
3
+ description: "Parallel-first sender research protocol. One round of five batched tool calls (fetch_linkedin_profile + fetch_company + 3× WebSearch), or the profile first and then the rest when only a profile URL is known; no enrich_sender required, ~30-40s wall time."
4
4
  visibility: internal
5
- allowed-tools: mcp__sellable__fetch_linkedin_profile mcp__sellable__fetch_company mcp__sellable__complete_sender_research WebSearch ToolSearch
5
+ allowed-tools: mcp__sellable__fetch_linkedin_profile mcp__sellable__fetch_company mcp__sellable__complete_sender_research WebSearch WebFetch ToolSearch
6
6
  ---
7
7
 
8
8
  # Research Sender (Parallel-First)
@@ -64,14 +64,24 @@ Concretely, your next assistant message must contain exactly these five
64
64
  `tool_use` blocks, in any order, with no leading or trailing prose:
65
65
 
66
66
  1. `fetch_linkedin_profile({ linkedinUrl })` — sender's LinkedIn profile (firstName/lastName, currentCompany, experience, follower count, education) for the brief's "sender background" section.
67
- 2. `fetch_company({ companyDomain })` — sender's company LinkedIn page (description, industry, employee range, recent posts). REQUIRED — replaces what `enrich_sender` used to provide for `companySnapshot`.
67
+ 2. `fetch_company({ companyUrl })` — sender's company LinkedIn page (description, industry, employee range, recent posts). `companyUrl` is the LinkedIn company URL. REQUIRED — replaces what `enrich_sender` used to provide for `companySnapshot`.
68
68
  3. `WebSearch({ query: 'site:{companyDomain} ("case study" OR "customer story" OR testimonial OR "success story") "{companyName}"' })` — proof.
69
69
  4. `WebSearch({ query: '"{companyName}" {companyDomain} {currentYear} (funding OR raised OR seed OR series OR hiring OR launch OR "press release")' })` — growth/credibility.
70
70
  5. `WebSearch({ query: '"{companyName}" about product site:{companyDomain}' })` — positioning.
71
71
 
72
+ **When only the profile URL is known** (a new customer gave nothing else), the
73
+ company name, domain and LinkedIn company URL come from the profile, so the
74
+ five calls cannot all go in one message. Call `fetch_linkedin_profile` alone
75
+ first, then emit the other four together in the next message. If the profile
76
+ does not show the company's website, call `fetch_company` next on its own and
77
+ take the website from it, then emit the three searches together. Never guess
78
+ the company or its domain from the handle. This order replaces "exactly these
79
+ five … in a single assistant message" above for this case.
80
+
72
81
  **Self-check before you reply:** if your reply contains fewer than five
73
- `tool_use` blocks (after the optional `ToolSearch` setup turn), you are
74
- violating the protocol. Stop, rewrite the reply with all five.
82
+ `tool_use` blocks (after the optional `ToolSearch` setup turn), or fewer than
83
+ four when the profile was fetched first, you are violating the protocol.
84
+ Stop, rewrite the reply with all of them.
75
85
 
76
86
  **Known limitation (`claude -p` headless mode):** Claude often serializes
77
87
  these calls one-per-turn even with explicit instructions. That's fine — the
@@ -137,6 +147,11 @@ signals, AND the campaign fixture/operator told you proof is required, you
137
147
  MAY issue ONE additional WebFetch on the most promising case-study URL from
138
148
  the proof WebSearch. Cap at one WebFetch. No subagents, no second WebSearch round.
139
149
 
150
+ If the proof and positioning searches returned nothing from the company's own
151
+ site (common when the company name is an ordinary word), you MAY issue that
152
+ ONE WebFetch on the company's homepage instead, to read what it says it does
153
+ and any customers it names. Same cap.
154
+
140
155
  If that still yields nothing, set `proofItemsFound: 0` in
141
156
  `complete_sender_research` and proceed; the brief can ship without proof.
142
157
 
@@ -157,16 +172,21 @@ If no reliable evidence is found, set counts to 0 and include that in `notes`.
157
172
 
158
173
  ## Progress UX
159
174
 
160
- Before issuing the parallel batch:
175
+ In a chat host where every line you write is posted to the customer (Slack),
176
+ write nothing here: the customer was already told research is under way, and
177
+ the next thing they should read is the result. Tool names, "in parallel",
178
+ time estimates and counts of proof items are not for customers.
179
+
180
+ In a terminal host, before issuing the batch:
161
181
 
162
182
  ```
163
- Pulling sender profile + company snapshot + case studies + growth signals + positioning in parallel (~10s)...
183
+ Researching the sender and company…
164
184
  ```
165
185
 
166
186
  After synthesis:
167
187
 
168
188
  ```
169
- Sender research ready — {proofItemsFound} proof items, {credibilitySignalsFound} credibility signals.
189
+ Sender research ready.
170
190
  ```
171
191
 
172
192
  ## What Changed From The Previous Protocol