@sellable/mcp 0.1.26 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index-dev.js CHANGED
File without changes
package/dist/index.js CHANGED
File without changes
package/dist/server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
- import { getSkillByName, listSkills, stripFrontmatter } from "./skills.js";
4
+ import { getSkillByName, listSkills } from "./skills.js";
5
5
  import { authToolDefinitions, getAuthStatus } from "./tools/auth.js";
6
6
  import { blueprintCommitToolDefinitions, handleAddColumn, handleCommitBlueprint, } from "./tools/blueprint-commit.js";
7
7
  import { bootstrapCreateCampaign, bootstrapToolDefinitions, } from "./tools/bootstrap.js";
@@ -67,6 +67,35 @@ const allTools = [
67
67
  ...blueprintCommitToolDefinitions,
68
68
  ...verifyRowToolDefinitions,
69
69
  ];
70
+ function parseOptionalNumber(value) {
71
+ if (typeof value === "number" && Number.isFinite(value))
72
+ return value;
73
+ if (typeof value === "string" && value.trim() !== "") {
74
+ const parsed = Number(value);
75
+ if (Number.isFinite(parsed))
76
+ return parsed;
77
+ }
78
+ return undefined;
79
+ }
80
+ function formatSubskillPromptText(result) {
81
+ const header = result.chunkCount && result.chunkCount > 1
82
+ ? [
83
+ `# ${result.name} prompt chunk ${result.chunkIndex} of ${result.chunkCount}`,
84
+ "",
85
+ `Prompt length: ${result.promptLength} characters.`,
86
+ result.chunkingInstructions ?? "",
87
+ "",
88
+ ].join("\n")
89
+ : "";
90
+ const footer = result.hasMore && result.nextOffset !== null
91
+ ? [
92
+ "",
93
+ "---",
94
+ `Continue with offset ${result.nextOffset} and limit ${result.limit}.`,
95
+ ].join("\n")
96
+ : "";
97
+ return `${header}${result.prompt}${footer}`;
98
+ }
70
99
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
71
100
  tools: allTools,
72
101
  }));
@@ -447,16 +476,29 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
447
476
  prompts: listSkills("public").map((skill) => ({
448
477
  name: skill.name,
449
478
  description: skill.description,
479
+ arguments: [
480
+ {
481
+ name: "offset",
482
+ description: "Optional character offset for chunked prompt reads.",
483
+ required: false,
484
+ },
485
+ {
486
+ name: "limit",
487
+ description: "Optional max characters to return for chunked prompt reads.",
488
+ required: false,
489
+ },
490
+ ],
450
491
  })),
451
492
  }));
452
493
  // Get prompt content
453
494
  server.setRequestHandler(GetPromptRequestSchema, async (request) => {
454
495
  const { name } = request.params;
496
+ const args = request.params.arguments ?? {};
455
497
  const skill = getSkillByName(name);
456
498
  if (!skill) {
457
499
  throw new Error(`Unknown prompt: ${name}`);
458
500
  }
459
- const content = stripFrontmatter(skill.content);
501
+ const content = getSubskillPrompt(name, parseOptionalNumber(args.offset), parseOptionalNumber(args.limit));
460
502
  return {
461
503
  description: skill.description,
462
504
  messages: [
@@ -464,7 +506,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
464
506
  role: "user",
465
507
  content: {
466
508
  type: "text",
467
- text: content,
509
+ text: formatSubskillPromptText(content),
468
510
  },
469
511
  },
470
512
  ],
@@ -215,10 +215,10 @@ export async function bootstrapCreateCampaign(input = {}) {
215
215
  : "";
216
216
  const nextStep = safeToProceed
217
217
  ? resumeDetected
218
- ? `Bootstrap complete.${workspaceNotice} Resume using campaign context, then load ${createCampaignSubskill?.name ?? "create-campaign"} instructions with get_subskill_prompt({ subskillName: "${createCampaignSubskill?.name ?? "create-campaign"}" }).`
218
+ ? `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.`
219
219
  : flowVersion === "v2"
220
- ? `Bootstrap complete.${workspaceNotice} Load create-campaign-v2 instructions with get_subskill_prompt({ subskillName: "create-campaign-v2" }), then start with the create-campaign-brief interview stage. Do not call create_campaign until the v2 approval gate returns approve.`
221
- : `Bootstrap complete.${workspaceNotice} Load ${createCampaignSubskill?.name ?? "create-campaign"} instructions with get_subskill_prompt({ subskillName: "${createCampaignSubskill?.name ?? "create-campaign"}" }) and follow that flow before calling create_campaign.`
220
+ ? `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.`
221
+ : `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.`
222
222
  : "Bootstrap incomplete. Resolve blockingErrors and rerun bootstrap_create_campaign before provider/search/import tools.";
223
223
  // Strip prompt body from createCampaignSubskill — it's loaded via the host
224
224
  // skill prompt. Only return metadata to save context tokens.
@@ -20,6 +20,9 @@ export interface SubskillPromptResponse {
20
20
  limit?: number;
21
21
  hasMore?: boolean;
22
22
  nextOffset?: number | null;
23
+ chunkIndex?: number;
24
+ chunkCount?: number;
25
+ chunkingInstructions?: string;
23
26
  }
24
27
  export interface ListSubskillPromptsResponse {
25
28
  total: number;
@@ -44,6 +47,8 @@ export interface CompleteSenderResearchInput {
44
47
  credibilitySignalsFound?: number;
45
48
  notes?: string;
46
49
  }
50
+ export declare const DEFAULT_SUBSKILL_PROMPT_CHUNK_CHARS = 24000;
51
+ export declare const MAX_SUBSKILL_PROMPT_CHUNK_CHARS = 24000;
47
52
  export declare const promptToolDefinitions: ({
48
53
  name: string;
49
54
  description: string;
@@ -65,6 +70,7 @@ export declare const promptToolDefinitions: ({
65
70
  required: never[];
66
71
  additionalProperties?: undefined;
67
72
  };
73
+ _meta?: undefined;
68
74
  } | {
69
75
  name: string;
70
76
  description: string;
@@ -95,9 +101,13 @@ export declare const promptToolDefinitions: ({
95
101
  required: never[];
96
102
  additionalProperties: boolean;
97
103
  };
104
+ _meta?: undefined;
98
105
  } | {
99
106
  name: string;
100
107
  description: string;
108
+ _meta: {
109
+ "anthropic/maxResultSizeChars": number;
110
+ };
101
111
  inputSchema: {
102
112
  type: string;
103
113
  properties: {
@@ -158,6 +168,7 @@ export declare const promptToolDefinitions: ({
158
168
  required: string[];
159
169
  additionalProperties: boolean;
160
170
  };
171
+ _meta?: undefined;
161
172
  } | {
162
173
  name: string;
163
174
  description: string;
@@ -195,6 +206,7 @@ export declare const promptToolDefinitions: ({
195
206
  additionalProperties: boolean;
196
207
  required?: undefined;
197
208
  };
209
+ _meta?: undefined;
198
210
  })[];
199
211
  export declare function getMessagePrompt(): Promise<PromptResponse>;
200
212
  export declare function listSubskillPrompts(limit?: number, includePublic?: boolean, includeInternal?: boolean): ListSubskillPromptsResponse;
@@ -1,6 +1,8 @@
1
1
  import { getApi } from "../api.js";
2
2
  import { getSkillByName, listSkills, stripFrontmatter } from "../skills.js";
3
3
  import { markCreateCampaignPromptLoaded, markResearchPromptLoaded, markSenderResearchCompleted, } from "./flow-preflight.js";
4
+ export const DEFAULT_SUBSKILL_PROMPT_CHUNK_CHARS = 24_000;
5
+ export const MAX_SUBSKILL_PROMPT_CHUNK_CHARS = 24_000;
4
6
  export const promptToolDefinitions = [
5
7
  {
6
8
  name: "get_message_prompt",
@@ -36,7 +38,10 @@ export const promptToolDefinitions = [
36
38
  },
37
39
  {
38
40
  name: "get_subskill_prompt",
39
- description: "Load a Sellable subskill prompt by name. Internal subskills are not listed in prompts/list; use search_subskill_prompts when you are unsure of the exact name.",
41
+ description: "Load a Sellable subskill prompt by name. Large prompts are returned in portable chunks by default; continue with nextOffset until hasMore is false. Internal subskills are not listed in prompts/list; use search_subskill_prompts when you are unsure of the exact name.",
42
+ _meta: {
43
+ "anthropic/maxResultSizeChars": 200_000,
44
+ },
40
45
  inputSchema: {
41
46
  type: "object",
42
47
  properties: {
@@ -50,7 +55,7 @@ export const promptToolDefinitions = [
50
55
  },
51
56
  limit: {
52
57
  type: "number",
53
- description: "Optional max characters to return for chunked prompt reads. Use a bounded value for very large prompts.",
58
+ description: `Optional max characters to return for chunked prompt reads. Defaults to ${DEFAULT_SUBSKILL_PROMPT_CHUNK_CHARS} for large prompts and is capped at ${MAX_SUBSKILL_PROMPT_CHUNK_CHARS}.`,
54
59
  },
55
60
  },
56
61
  required: ["subskillName"],
@@ -141,11 +146,7 @@ export function listSubskillPrompts(limit, includePublic, includeInternal) {
141
146
  })),
142
147
  };
143
148
  }
144
- export function getSubskillPrompt(subskillName, offset, limit) {
145
- const skill = getSkillByName(subskillName);
146
- if (!skill) {
147
- throw new Error(`Unknown subskill prompt: ${subskillName}`);
148
- }
149
+ function markSubskillPromptLoaded(subskillName) {
149
150
  if (subskillName === "create-campaign") {
150
151
  markCreateCampaignPromptLoaded();
151
152
  }
@@ -158,17 +159,37 @@ export function getSubskillPrompt(subskillName, offset, limit) {
158
159
  if (subskillName === "research-prospect") {
159
160
  markResearchPromptLoaded("prospect", "research-prospect");
160
161
  }
162
+ }
163
+ export function getSubskillPrompt(subskillName, offset, limit) {
164
+ const skill = getSkillByName(subskillName);
165
+ if (!skill) {
166
+ throw new Error(`Unknown subskill prompt: ${subskillName}`);
167
+ }
168
+ markSubskillPromptLoaded(subskillName);
161
169
  const fullPrompt = stripFrontmatter(skill.content);
162
170
  const safeOffset = Math.min(Math.max(Number.isFinite(offset) ? Math.floor(offset ?? 0) : 0, 0), fullPrompt.length);
163
- const safeLimit = Number.isFinite(limit) && limit !== undefined
171
+ const requestedLimit = Number.isFinite(limit) && limit !== undefined
164
172
  ? Math.max(Math.floor(limit), 1)
165
173
  : null;
174
+ const defaultChunkLimit = fullPrompt.length > DEFAULT_SUBSKILL_PROMPT_CHUNK_CHARS
175
+ ? DEFAULT_SUBSKILL_PROMPT_CHUNK_CHARS
176
+ : null;
177
+ const safeLimit = requestedLimit === null
178
+ ? defaultChunkLimit
179
+ : Math.min(requestedLimit, MAX_SUBSKILL_PROMPT_CHUNK_CHARS);
166
180
  const prompt = safeLimit === null
167
181
  ? fullPrompt
168
182
  : fullPrompt.slice(safeOffset, safeOffset + safeLimit);
169
183
  const nextOffset = safeLimit === null
170
184
  ? null
171
185
  : Math.min(safeOffset + safeLimit, fullPrompt.length);
186
+ const hasMore = nextOffset !== null && nextOffset < fullPrompt.length;
187
+ const chunkCount = safeLimit === null
188
+ ? 1
189
+ : Math.max(Math.ceil(fullPrompt.length / safeLimit), 1);
190
+ const chunkIndex = safeLimit === null
191
+ ? 1
192
+ : Math.min(Math.floor(safeOffset / safeLimit) + 1, chunkCount);
172
193
  return {
173
194
  name: skill.name,
174
195
  description: skill.description,
@@ -177,8 +198,13 @@ export function getSubskillPrompt(subskillName, offset, limit) {
177
198
  promptLength: fullPrompt.length,
178
199
  offset: safeOffset,
179
200
  limit: safeLimit ?? undefined,
180
- hasMore: nextOffset !== null && nextOffset < fullPrompt.length,
181
- nextOffset: nextOffset !== null && nextOffset < fullPrompt.length ? nextOffset : null,
201
+ hasMore,
202
+ nextOffset: hasMore ? nextOffset : null,
203
+ chunkIndex,
204
+ chunkCount,
205
+ chunkingInstructions: hasMore
206
+ ? `Continue this same prompt load with get_subskill_prompt({ subskillName: "${skill.name}", offset: ${nextOffset}, limit: ${safeLimit} }) until hasMore is false. Treat all chunks as one prompt load; do not use saved local tool-output files or machine-specific paths.`
207
+ : undefined,
182
208
  };
183
209
  }
184
210
  export function completeSenderResearch(input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  # Clover Campaign Brief + Rubric Archive
2
2
 
3
- Source: [75-ALL-CLIENT-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ALL-CLIENT-CAMPAIGN-CORPUS.json)
3
+ Source: Phase 75 all-client campaign corpus, copied into this prompt reference archive.
4
4
 
5
5
  Campaigns captured: 4
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Galley Campaign Brief + Rubric Archive
2
2
 
3
- Source: [75-ALL-CLIENT-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ALL-CLIENT-CAMPAIGN-CORPUS.json)
3
+ Source: Phase 75 all-client campaign corpus, copied into this prompt reference archive.
4
4
 
5
5
  Campaigns captured: 5
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Gelee Campaign Brief + Rubric Archive
2
2
 
3
- Source: [75-ALL-CLIENT-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ALL-CLIENT-CAMPAIGN-CORPUS.json)
3
+ Source: Phase 75 all-client campaign corpus, copied into this prompt reference archive.
4
4
 
5
5
  Campaigns captured: 1
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Hey Digital Campaign Brief + Rubric Archive
2
2
 
3
- Source: [75-ALL-CLIENT-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ALL-CLIENT-CAMPAIGN-CORPUS.json)
3
+ Source: Phase 75 all-client campaign corpus, copied into this prompt reference archive.
4
4
 
5
5
  Campaigns captured: 4
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Persona Campaign Brief + Rubric Archive
2
2
 
3
- Source: [75-ALL-CLIENT-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ALL-CLIENT-CAMPAIGN-CORPUS.json)
3
+ Source: Phase 75 all-client campaign corpus, copied into this prompt reference archive.
4
4
 
5
5
  Campaigns captured: 3
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Revvix Campaign Brief + Rubric Archive
2
2
 
3
- Source: [75-ALL-CLIENT-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ALL-CLIENT-CAMPAIGN-CORPUS.json)
3
+ Source: Phase 75 all-client campaign corpus, copied into this prompt reference archive.
4
4
 
5
5
  Campaigns captured: 8
6
6
 
@@ -1,6 +1,6 @@
1
1
  # sellable.dev Campaign Brief + Rubric Archive
2
2
 
3
- Source: [75-ALL-CLIENT-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ALL-CLIENT-CAMPAIGN-CORPUS.json)
3
+ Source: Phase 75 all-client campaign corpus, copied into this prompt reference archive.
4
4
 
5
5
  Campaigns captured: 304
6
6
 
@@ -1,6 +1,6 @@
1
1
  # superposition Campaign Brief + Rubric Archive
2
2
 
3
- Source: [75-ALL-CLIENT-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ALL-CLIENT-CAMPAIGN-CORPUS.json)
3
+ Source: Phase 75 all-client campaign corpus, copied into this prompt reference archive.
4
4
 
5
5
  Campaigns captured: 5
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Superpower Campaign Brief + Rubric Archive
2
2
 
3
- Source: [75-ALL-CLIENT-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ALL-CLIENT-CAMPAIGN-CORPUS.json)
3
+ Source: Phase 75 all-client campaign corpus, copied into this prompt reference archive.
4
4
 
5
5
  Campaigns captured: 3
6
6
 
@@ -1,6 +1,6 @@
1
1
  # westpark villas Campaign Brief + Rubric Archive
2
2
 
3
- Source: [75-ALL-CLIENT-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ALL-CLIENT-CAMPAIGN-CORPUS.json)
3
+ Source: Phase 75 all-client campaign corpus, copied into this prompt reference archive.
4
4
 
5
5
  Campaigns captured: 26
6
6
 
@@ -167,7 +167,7 @@ Add only when the campaign truly needs them:
167
167
 
168
168
  Reference:
169
169
 
170
- - [75-MESSAGE-VARIANT-GENERATION-FLOW.md](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-MESSAGE-VARIANT-GENERATION-FLOW.md)
170
+ - `75-MESSAGE-VARIANT-GENERATION-FLOW.md` in the Phase 75 planning archive.
171
171
 
172
172
  ## Section Rules
173
173
 
@@ -1,10 +1,10 @@
1
1
  # Phase 75 Live Brief + Messaging Example Bank
2
2
 
3
- Source of truth: [75-LIVE-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-LIVE-CAMPAIGN-CORPUS.json)
3
+ Source of truth: Phase 75 live campaign corpus used to generate this prompt reference.
4
4
 
5
- Full archive of client campaign briefs: [75-ALL-CLIENT-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ALL-CLIENT-CAMPAIGN-CORPUS.json)
5
+ Full archive of client campaign briefs: Phase 75 all-client campaign corpus, copied into this prompt reference archive.
6
6
 
7
- Direct copied archive of full campaign brief content + rubrics: [brief-rubric-archive/INDEX.md](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/brief-rubric-archive/INDEX.md)
7
+ Direct copied archive of full campaign brief content + rubrics: `brief-rubric-archive/INDEX.md` in the Phase 75 planning archive.
8
8
 
9
9
  This file distills the strongest live campaign briefs currently stored in production. The runtime gold set should stay narrow: active clients only, plus the one Hey Digital campaign that clearly performed and still teaches useful structure. The goal is not to preserve every brief shape forever. The goal is to preserve the patterns that consistently show up in the briefs that are actually usable, persuasive, and close to what Sellable has been shipping manually for clients.
10
10
 
@@ -20,7 +20,7 @@ Important note:
20
20
 
21
21
  Runtime gold pack:
22
22
 
23
- - [75-ACTIVE-RUNTIME-MESSAGE-PACK.md](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ACTIVE-RUNTIME-MESSAGE-PACK.md)
23
+ - `75-ACTIVE-RUNTIME-MESSAGE-PACK.md` in the Phase 75 planning archive.
24
24
 
25
25
  ## Winning Campaigns In Scope
26
26
 
@@ -482,7 +482,9 @@ Sellable lead lists in the main three-option first batch; support them through
482
482
  custom/freeform input. If the user pastes up to 100 LinkedIn profile URLs or
483
483
  company domains, normalize the paste into a temporary local CSV and continue
484
484
  through the matching CSV preview path. Mixed, ambiguous, malformed, or oversized
485
- pastes should ask for a real CSV file instead of guessing.
485
+ pastes should ask for a real CSV file instead of guessing. Uploaded CSV support
486
+ is larger than paste support: LinkedIn profile CSVs can contain up to 7,500
487
+ rows; domain CSVs can contain up to 7,500 rows but only 1,000 unique domains.
486
488
 
487
489
  Avoid internal wording like `Which proof points should the message be allowed
488
490
  to lean on?` because it describes the artifact, not the founder decision.
@@ -555,6 +557,10 @@ usable leads`.
555
557
  - supplied LinkedIn profile CSVs call `load_csv_linkedin_leads` in preview mode
556
558
  only before approval; do not pass `confirmed: true`, `campaignOfferId`,
557
559
  `currentStep`, `leadListId`, `sourceLeadListId`, or provider-import params
560
+ during the preview. After approval, this branch is a two-stage batch path:
561
+ materialize the full supplied CSV into a Sellable lead-list table first, then
562
+ use the returned `leadListId` as `sourceLeadListId` for the bounded campaign
563
+ review import
558
564
  - supplied company/domain CSVs call `load_csv_domains`; pre-approval
559
565
  confirmation is allowed only without `campaignOfferId` or `currentStep` and
560
566
  only to produce a standalone `domainFilterId` for campaignless
@@ -766,12 +772,13 @@ Run the `generate-messages` skill in caller-declared `DRY MODE`. Its SKILL.md
766
772
  holds the full drafting contract (retrieval, proof inventory, candidates,
767
773
  finalizer pass, voice rules, safety). This step only covers orchestration.
768
774
  This is not optional: before any write to `message-validation.md`,
769
- `message-review.md`, `approval-packet.md`, or a commit-gate question, the current run must load the
770
- full `generate-messages` prompt. In hosted/from-scratch runs, load it with
771
- chunked `get_subskill_prompt({ subskillName: "generate-messages", offset,
772
- limit })` calls so every tool result stays small enough for the streamed
773
- harness. Start with `offset: 0, limit: 12000`, then keep calling with
774
- `offset: nextOffset` until `hasMore` is false.
775
+ `message-review.md`, `approval-packet.md`, or a commit-gate question, the
776
+ current run must load the full `generate-messages` prompt. In hosted/from-
777
+ scratch runs, load it with chunked `get_subskill_prompt({ subskillName:
778
+ "generate-messages", offset, limit })` calls so every tool result stays small
779
+ enough for the streamed harness. Start with `offset: 0`; the MCP will apply a
780
+ portable default limit when omitted. Keep calling with `offset: nextOffset`
781
+ until `hasMore` is false.
775
782
  In Codex-hosted runs, this is a quality gate, not just provenance: if the
776
783
  model cannot retrieve the complete prompt or cannot follow the required
777
784
  gold-standard deliberation flow, stop at `message-review` and ask for
@@ -1648,9 +1655,9 @@ Message` column's http_request writes those cells via the cascade.
1648
1655
  Load `core/auto-execute.yaml` exactly once at the start of Step 13. All
1649
1656
  subsequent steps read the already-parsed config. Do not re-load mid-run.
1650
1657
  Load each subskill prompt (`create-campaign-v2`, `research-sender`,
1651
- `generate-messages`) at most once per run if a tool result already
1652
- told you to load a prompt, load it and remember; do not re-request the
1653
- same prompt later.
1658
+ `generate-messages`) at most once per run. A multi-call chunk sequence counts
1659
+ as one load. If a tool result already told you to load a prompt, load all
1660
+ chunks once and remember; do not restart the same prompt from offset 0 later.
1654
1661
 
1655
1662
  After every `update_campaign({ currentStep: ... })` in the tail, narrate
1656
1663
  what changed and orient the user to what the watch link will show next —
@@ -1707,10 +1714,12 @@ atomic mint).
1707
1714
  `import_leads({ campaignOfferId, targetLeadCount: importLimit })`.
1708
1715
  If a manifest exists, branch by `sourceType`:
1709
1716
  - `supplied-linkedin-profiles`: revalidate file metadata and confirmation
1710
- token, confirm `load_csv_linkedin_leads` only after approval, persist the
1711
- returned `leadListId`, then call
1712
- `confirm_lead_list({ sourceLeadListId: leadListId, targetLeadCount:
1713
- importLimit })`.
1717
+ token, confirm `load_csv_linkedin_leads` only after approval to batch the
1718
+ supplied CSV into a Sellable lead list, persist the returned `leadListId`,
1719
+ then call `confirm_lead_list({ sourceLeadListId: leadListId,
1720
+ targetLeadCount: importLimit })`. Do not call `import_leads` for this
1721
+ branch; the lead list is the source, and `confirm_lead_list` imports the
1722
+ bounded review batch into the campaign table.
1714
1723
  - `existing-lead-list`: revalidate that the lead list still exists in the
1715
1724
  same workspace, reuse it as `sourceLeadListId`, then call
1716
1725
  `confirm_lead_list({ sourceLeadListId, targetLeadCount: importLimit })`.
@@ -345,7 +345,7 @@
345
345
  },
346
346
  "optionalRequiredArtifacts": ["lead-source-intake.json"],
347
347
  "toolRules": {
348
- "suppliedLinkedinProfiles": "Preview only before approval: call load_csv_linkedin_leads without confirmed, campaignOfferId, currentStep, leadListId, sourceLeadListId, or provider-import parameters; skip provider discovery.",
348
+ "suppliedLinkedinProfiles": "Preview only before approval: call load_csv_linkedin_leads without confirmed, campaignOfferId, currentStep, leadListId, sourceLeadListId, or provider-import parameters; skip provider discovery. After approval, batch/materialize the supplied CSV into a Sellable lead list first, then use the returned leadListId as sourceLeadListId for confirm_lead_list.",
349
349
  "suppliedDomains": "May confirm load_csv_domains before approval only without campaignOfferId/currentStep to produce a standalone domainFilterId, then run a campaignless Prospeo sample constrained by domainFilterId.",
350
350
  "existingLeadList": "Skip provider discovery and sample existing rows before approval; do not clone/import until after approval.",
351
351
  "forbiddenPreApproval": [
@@ -1022,7 +1022,7 @@
1022
1022
  "defaultWhenMissing": "discovered-provider-import",
1023
1023
  "branches": {
1024
1024
  "normal-discovery": "import_leads({ campaignOfferId, targetLeadCount: import.importLimit })",
1025
- "supplied-linkedin-profiles": "confirm load_csv_linkedin_leads after approval, then confirm_lead_list({ sourceLeadListId, targetLeadCount: import.importLimit })",
1025
+ "supplied-linkedin-profiles": "confirm load_csv_linkedin_leads after approval to batch/materialize the CSV into a lead list, persist returned leadListId, then confirm_lead_list({ sourceLeadListId: leadListId, targetLeadCount: import.importLimit }); do not call import_leads for this branch",
1026
1026
  "existing-lead-list": "confirm_lead_list({ sourceLeadListId: existingLeadListId, targetLeadCount: import.importLimit })",
1027
1027
  "supplied-domains": "reuse or confirm load_csv_domains, run campaign-associated search_prospeo({ campaignOfferId, domainFilterId }), then import_leads({ campaignOfferId, targetLeadCount: import.importLimit })"
1028
1028
  },
@@ -23,8 +23,12 @@ Supported branches:
23
23
 
24
24
  - **No manifest / normal discovery** — use the existing provider import path.
25
25
  - **Supplied LinkedIn profile CSV** — confirm `load_csv_linkedin_leads` only
26
- after approval, then import into the campaign table with
27
- `confirm_lead_list({ sourceLeadListId, targetLeadCount: importLimit })`.
26
+ after approval to batch/materialize the full uploaded CSV into a Sellable
27
+ lead-list table. Persist the returned `leadListId`, then import the bounded
28
+ review batch into the campaign table with
29
+ `confirm_lead_list({ sourceLeadListId: leadListId, targetLeadCount: importLimit })`.
30
+ Do not call `import_leads` for this branch; the materialized lead list is the
31
+ source.
28
32
  Do **not** call `wait_for_lead_list_ready` for this branch; there is no
29
33
  provider import job to wait on, and probing readiness can leave the campaign
30
34
  in an import-failed state before `confirm_lead_list`.