@sellable/mcp 0.1.25 → 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.
Files changed (24) hide show
  1. package/README.md +2 -2
  2. package/dist/index-dev.js +0 -0
  3. package/dist/index.js +0 -0
  4. package/dist/server.js +45 -3
  5. package/dist/tools/bootstrap.js +3 -3
  6. package/dist/tools/prompts.d.ts +12 -0
  7. package/dist/tools/prompts.js +36 -10
  8. package/package.json +1 -1
  9. package/skills/create-campaign-brief/references/examples/briefs/clover.md +1 -1
  10. package/skills/create-campaign-brief/references/examples/briefs/galley.md +1 -1
  11. package/skills/create-campaign-brief/references/examples/briefs/gelee.md +1 -1
  12. package/skills/create-campaign-brief/references/examples/briefs/hey-digital.md +1 -1
  13. package/skills/create-campaign-brief/references/examples/briefs/persona.md +1 -1
  14. package/skills/create-campaign-brief/references/examples/briefs/revvix.md +1 -1
  15. package/skills/create-campaign-brief/references/examples/briefs/sellable-dev.md +1 -1
  16. package/skills/create-campaign-brief/references/examples/briefs/superposition.md +1 -1
  17. package/skills/create-campaign-brief/references/examples/briefs/superpower.md +1 -1
  18. package/skills/create-campaign-brief/references/examples/briefs/westpark-villas.md +1 -1
  19. package/skills/create-campaign-brief/references/phase75-canonical-brief-template.md +1 -1
  20. package/skills/create-campaign-brief/references/phase75-good-brief-and-messaging-examples.md +4 -4
  21. package/skills/create-campaign-v2/SKILL.md +58 -45
  22. package/skills/create-campaign-v2/SOUL.md +13 -7
  23. package/skills/create-campaign-v2/core/flow.v2.json +2 -2
  24. package/skills/create-campaign-v2/references/step-13-import-leads.md +6 -2
package/README.md CHANGED
@@ -83,14 +83,14 @@ The token is provided when you generate it. Use `list_workspaces` +
83
83
  For customer/package installs, use the public installer:
84
84
 
85
85
  ```bash
86
- npx -y @sellable/install@0.1.25 --host codex --token skt_live_your_token_here --workspace-id your_workspace_id
86
+ npx -y @sellable/install@0.1.26 --host codex --token skt_live_your_token_here --workspace-id your_workspace_id
87
87
  ```
88
88
 
89
89
  If you already have `~/.sellable/config.json`, rerun/verify without rewriting
90
90
  auth:
91
91
 
92
92
  ```bash
93
- npx -y @sellable/install@0.1.25 --host codex
93
+ npx -y @sellable/install@0.1.26 --host codex
94
94
  sellable --verify-only --host codex
95
95
  ```
96
96
 
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.25",
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
 
@@ -79,7 +79,11 @@ Validated draft directory:
79
79
  `request_user_input` (enabled in Default mode by
80
80
  `[features].default_mode_request_user_input = true`, not available in
81
81
  `codex exec`). Treat them as equivalent approval/intake gates and persist the
82
- same draft artifacts after the user answers. If an interactive
82
+ same draft artifacts after the user answers. Use this structured gate only for
83
+ multiple-choice decisions or approval gates. Never use it to collect open text
84
+ input like LinkedIn URLs, company domains, notes, pasted context, campaign
85
+ ideas, or feedback. For open text, ask in normal chat and wait for the user to
86
+ paste the value. If an interactive
83
87
  Codex session does not expose `request_user_input`, do not silently degrade to
84
88
  a plain chat question; stop and tell the user:
85
89
 
@@ -141,10 +145,10 @@ Validated draft directory:
141
145
  the campaign subject and sender before continuing.
142
146
  - If the user did not provide the launch identity, quietly call `list_senders`
143
147
  once if available. This is a shortcut to deduce who the user might be from
144
- their Sellable API token and connected LinkedIn accounts. Do not present it as
145
- a sender picker yet. If there is one strong likely sender, use `enrich_sender`
146
- to infer their current or most recent company, then ask a structured
147
- confirmation question:
148
+ their Sellable API token and connected LinkedIn accounts. Do not ask the user
149
+ to pick an input type before checking connected senders. If there is any likely
150
+ connected sender, use `enrich_sender` on the best match to infer their current
151
+ or most recent company, then ask a structured confirmation question:
148
152
 
149
153
  ```text
150
154
  I’m ready to build this in {workspace}. I found {matched sender} connected here.
@@ -152,25 +156,30 @@ Validated draft directory:
152
156
  Is that you, and is this campaign for {company}?
153
157
  ```
154
158
 
155
- The options must be:
159
+ The structured options must be no more than three choices:
156
160
 
157
- 1. `Yes, that's me and this is for {company}`
158
- 2. `That's me, but this is for a different company`
159
- 3. `No, I'll paste my LinkedIn URL so you can look me up`
160
- 4. `No, I'll paste the company website instead`
161
- 5. `Other / custom`
161
+ 1. `Yes use {matched sender} for {company}`
162
+ 2. `No I'll paste a LinkedIn profile`
163
+ 3. `Use a company domain instead`
162
164
 
163
- If the user chooses option 2, ask for the company website/domain and call
164
- `fetch_company` when possible, otherwise one web lookup. If the user chooses
165
- option 3, ask for their LinkedIn URL, call `fetch_linkedin_profile`, infer
166
- their current or most recent company, then confirm company and sender again.
167
- If the user chooses option 4, ask for the company website/domain, call
168
- `fetch_company` when possible, otherwise one web lookup, then ask who the
169
- LinkedIn messages should send from.
165
+ If there are multiple likely connected senders, mention the best one in the
166
+ question and use option 2 for either a different connected sender or a pasted
167
+ LinkedIn profile.
168
+
169
+ Use the structured question tool only for the choice. Do not use
170
+ `request_user_input`/`AskUserQuestion` to collect a LinkedIn URL, company
171
+ domain, or freeform text. If the user chooses option 2, ask in normal chat:
172
+ `Paste the LinkedIn URL I should use, and I’ll look it up.` Then call
173
+ `fetch_linkedin_profile`, infer their current or most recent company, and
174
+ confirm company and sender again. If the user chooses option 3, ask in normal
175
+ chat: `Paste the company domain, and I’ll do a quick lookup before we keep
176
+ going.` Then call `fetch_company` when possible, otherwise one web lookup, and
177
+ ask who the LinkedIn messages should send from.
170
178
 
171
179
  If `list_senders` returns zero connected senders, avoid the sender-confirmation
172
- branch entirely. Ask for the user's LinkedIn URL or the company they want to
173
- send on behalf of so you can research context:
180
+ branch entirely. Do not ask the user to choose an input type with the
181
+ structured question tool. Ask in normal chat for the user's LinkedIn URL or the
182
+ company they want to send on behalf of so you can research context:
174
183
 
175
184
  ```text
176
185
  I’m ready to build this in {workspace}.
@@ -180,11 +189,11 @@ Validated draft directory:
180
189
  offer, proof, and lead source.
181
190
  ```
182
191
 
183
- If there is no strong sender match, make the first setup choice ask for the
184
- user's LinkedIn URL or company website. The point of this gate is not "pick a
185
- sender"; it is to learn who the user is, infer the current or most recent
186
- company, and then confirm who we are sending from. The customer-facing shape
187
- should be:
192
+ If there is no strong sender match, do not show a structured choice that says
193
+ "LinkedIn profile" vs "Company website". The point of this gate is not "pick a
194
+ sender" or "pick an input type"; it is to learn who the user is, infer the
195
+ current or most recent company, and then confirm who we are sending from. The
196
+ customer-facing shape should be:
188
197
 
189
198
  ```text
190
199
  I’m ready to build this in {workspace}.
@@ -193,12 +202,7 @@ Validated draft directory:
193
202
  the company website instead.
194
203
  ```
195
204
 
196
- The LinkedIn/company identity gate should ask:
197
-
198
- 1. `What’s your LinkedIn URL?` Options: `I’ll paste my LinkedIn profile`,
199
- `I’ll paste the company website instead`, `Other / custom`.
200
-
201
- After that answer, do the lightweight lookup. For a LinkedIn profile, call
205
+ After the user pastes a URL/domain, do the lightweight lookup. For a LinkedIn profile, call
202
206
  `fetch_linkedin_profile` and infer the user's current or most recent company
203
207
  from the profile. For a company website, call `fetch_company` when possible,
204
208
  otherwise one web lookup.
@@ -478,7 +482,9 @@ Sellable lead lists in the main three-option first batch; support them through
478
482
  custom/freeform input. If the user pastes up to 100 LinkedIn profile URLs or
479
483
  company domains, normalize the paste into a temporary local CSV and continue
480
484
  through the matching CSV preview path. Mixed, ambiguous, malformed, or oversized
481
- 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.
482
488
 
483
489
  Avoid internal wording like `Which proof points should the message be allowed
484
490
  to lean on?` because it describes the artifact, not the founder decision.
@@ -551,6 +557,10 @@ usable leads`.
551
557
  - supplied LinkedIn profile CSVs call `load_csv_linkedin_leads` in preview mode
552
558
  only before approval; do not pass `confirmed: true`, `campaignOfferId`,
553
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
554
564
  - supplied company/domain CSVs call `load_csv_domains`; pre-approval
555
565
  confirmation is allowed only without `campaignOfferId` or `currentStep` and
556
566
  only to produce a standalone `domainFilterId` for campaignless
@@ -762,12 +772,13 @@ Run the `generate-messages` skill in caller-declared `DRY MODE`. Its SKILL.md
762
772
  holds the full drafting contract (retrieval, proof inventory, candidates,
763
773
  finalizer pass, voice rules, safety). This step only covers orchestration.
764
774
  This is not optional: before any write to `message-validation.md`,
765
- `message-review.md`, `approval-packet.md`, or a commit-gate question, the current run must load the
766
- full `generate-messages` prompt. In hosted/from-scratch runs, load it with
767
- chunked `get_subskill_prompt({ subskillName: "generate-messages", offset,
768
- limit })` calls so every tool result stays small enough for the streamed
769
- harness. Start with `offset: 0, limit: 12000`, then keep calling with
770
- `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.
771
782
  In Codex-hosted runs, this is a quality gate, not just provenance: if the
772
783
  model cannot retrieve the complete prompt or cannot follow the required
773
784
  gold-standard deliberation flow, stop at `message-review` and ask for
@@ -1644,9 +1655,9 @@ Message` column's http_request writes those cells via the cascade.
1644
1655
  Load `core/auto-execute.yaml` exactly once at the start of Step 13. All
1645
1656
  subsequent steps read the already-parsed config. Do not re-load mid-run.
1646
1657
  Load each subskill prompt (`create-campaign-v2`, `research-sender`,
1647
- `generate-messages`) at most once per run if a tool result already
1648
- told you to load a prompt, load it and remember; do not re-request the
1649
- 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.
1650
1661
 
1651
1662
  After every `update_campaign({ currentStep: ... })` in the tail, narrate
1652
1663
  what changed and orient the user to what the watch link will show next —
@@ -1703,10 +1714,12 @@ atomic mint).
1703
1714
  `import_leads({ campaignOfferId, targetLeadCount: importLimit })`.
1704
1715
  If a manifest exists, branch by `sourceType`:
1705
1716
  - `supplied-linkedin-profiles`: revalidate file metadata and confirmation
1706
- token, confirm `load_csv_linkedin_leads` only after approval, persist the
1707
- returned `leadListId`, then call
1708
- `confirm_lead_list({ sourceLeadListId: leadListId, targetLeadCount:
1709
- 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.
1710
1723
  - `existing-lead-list`: revalidate that the lead list still exists in the
1711
1724
  same workspace, reuse it as `sourceLeadListId`, then call
1712
1725
  `confirm_lead_list({ sourceLeadListId, targetLeadCount: importLimit })`.
@@ -146,10 +146,10 @@ brief before anything is created.
146
146
  Good identity setup:
147
147
 
148
148
  ```text
149
- I’ll check whether you already have a connected LinkedIn account here. If I cant
150
- confirm it, Ill ask for your LinkedIn URL or company website and use that to
151
- understand the company before we choose the target, offer, proof, and lead
152
- source.
149
+ I’ll check whether you already have a connected sender here. If I find one, Ill
150
+ ask whether thats you and whether this campaign is for that company. If not,
151
+ just paste your LinkedIn URL or company domain and I’ll look it up before we
152
+ keep going.
153
153
  ```
154
154
 
155
155
  Bad:
@@ -167,9 +167,15 @@ First I’ll check whether your Sellable token already tells me who you are.
167
167
  Better:
168
168
 
169
169
  ```text
170
- I’ll first check whether you already have a connected sender here. If not, I’ll
171
- ask for your LinkedIn URL or company website and use that to understand the
172
- campaign before we choose the audience and offer.
170
+ I found Christian Reyes connected here. Is that you, and is this campaign for
171
+ Sellable? If not, choose LinkedIn profile or company domain and I’ll ask you to
172
+ paste it in chat.
173
+ ```
174
+
175
+ Bad:
176
+
177
+ ```text
178
+ What should I use to confirm who this campaign is for?
173
179
  ```
174
180
 
175
181
  Bad:
@@ -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`.