@sellable/mcp 0.1.60 → 0.1.62

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/server.js CHANGED
@@ -21,7 +21,7 @@ import { fetchCompany, fetchCompanyPosts, fetchLinkedInPosts, fetchLinkedInProfi
21
21
  import { getCampaignNavigationState, navigationToolDefinitions, } from "./tools/navigation.js";
22
22
  import { addOnDemandLeads, createOnDemandCampaign, createOnDemandTable, initOnDemandSequence, onDemandToolDefinitions, pauseOnDemandCampaign, startOnDemandCampaign, } from "./tools/one-off.js";
23
23
  import { processingToolDefinitions, upsertRubric } from "./tools/processing.js";
24
- import { completeSenderResearch, getMessagePrompt, getSubskillPrompt, listSubskillPrompts, promptToolDefinitions, searchSubskillPrompts, } from "./tools/prompts.js";
24
+ import { completeSenderResearch, getMessagePrompt, getSubskillAsset, getSubskillPrompt, listSubskillPrompts, promptToolDefinitions, searchSubskillPrompts, } from "./tools/prompts.js";
25
25
  import { readinessToolDefinitions, waitForCampaignTableReady, waitForLeadListReady, } from "./tools/readiness.js";
26
26
  import { getRows, getTableRows, getTableRowsMinimal, rowToolDefinitions, } from "./tools/rows.js";
27
27
  import { addRubricItem, checkRubric, deleteRubricItem, draftRubrics, rubricToolDefinitions, saveRubrics, selectNecessaryRubrics, updateRubricItem, waitForRubricResults, } from "./tools/rubrics.js";
@@ -399,6 +399,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
399
399
  case "get_subskill_prompt":
400
400
  result = getSubskillPrompt(args?.subskillName, args?.offset, args?.limit);
401
401
  break;
402
+ case "get_subskill_asset":
403
+ result = getSubskillAsset(args?.subskillName, args?.assetPath, args?.offset, args?.limit);
404
+ break;
402
405
  case "list_subskill_prompts":
403
406
  result = listSubskillPrompts(args?.limit, args?.includePublic, args?.includeInternal);
404
407
  break;
@@ -24,6 +24,19 @@ export interface SubskillPromptResponse {
24
24
  chunkCount?: number;
25
25
  chunkingInstructions?: string;
26
26
  }
27
+ export interface SubskillAssetResponse {
28
+ subskillName: string;
29
+ assetPath: string;
30
+ content: string;
31
+ contentLength: number;
32
+ offset?: number;
33
+ limit?: number;
34
+ hasMore: boolean;
35
+ nextOffset: number | null;
36
+ chunkIndex: number;
37
+ chunkCount: number;
38
+ chunkingInstructions?: string;
39
+ }
27
40
  export interface ListSubskillPromptsResponse {
28
41
  total: number;
29
42
  matches: Array<{
@@ -60,6 +73,7 @@ export declare const promptToolDefinitions: ({
60
73
  includeInternal?: undefined;
61
74
  subskillName?: undefined;
62
75
  offset?: undefined;
76
+ assetPath?: undefined;
63
77
  query?: undefined;
64
78
  depth?: undefined;
65
79
  proofItemsFound?: undefined;
@@ -91,6 +105,7 @@ export declare const promptToolDefinitions: ({
91
105
  };
92
106
  subskillName?: undefined;
93
107
  offset?: undefined;
108
+ assetPath?: undefined;
94
109
  query?: undefined;
95
110
  depth?: undefined;
96
111
  proofItemsFound?: undefined;
@@ -125,6 +140,44 @@ export declare const promptToolDefinitions: ({
125
140
  };
126
141
  includePublic?: undefined;
127
142
  includeInternal?: undefined;
143
+ assetPath?: undefined;
144
+ query?: undefined;
145
+ depth?: undefined;
146
+ proofItemsFound?: undefined;
147
+ caseStudyItemsFound?: undefined;
148
+ credibilitySignalsFound?: undefined;
149
+ notes?: undefined;
150
+ };
151
+ required: string[];
152
+ additionalProperties: boolean;
153
+ };
154
+ } | {
155
+ name: string;
156
+ description: string;
157
+ _meta: {
158
+ "anthropic/maxResultSizeChars": number;
159
+ };
160
+ inputSchema: {
161
+ type: string;
162
+ properties: {
163
+ subskillName: {
164
+ type: string;
165
+ description: string;
166
+ };
167
+ assetPath: {
168
+ type: string;
169
+ description: string;
170
+ };
171
+ offset: {
172
+ type: string;
173
+ description: string;
174
+ };
175
+ limit: {
176
+ type: string;
177
+ description: string;
178
+ };
179
+ includePublic?: undefined;
180
+ includeInternal?: undefined;
128
181
  query?: undefined;
129
182
  depth?: undefined;
130
183
  proofItemsFound?: undefined;
@@ -159,6 +212,7 @@ export declare const promptToolDefinitions: ({
159
212
  };
160
213
  subskillName?: undefined;
161
214
  offset?: undefined;
215
+ assetPath?: undefined;
162
216
  depth?: undefined;
163
217
  proofItemsFound?: undefined;
164
218
  caseStudyItemsFound?: undefined;
@@ -201,6 +255,7 @@ export declare const promptToolDefinitions: ({
201
255
  includeInternal?: undefined;
202
256
  subskillName?: undefined;
203
257
  offset?: undefined;
258
+ assetPath?: undefined;
204
259
  query?: undefined;
205
260
  };
206
261
  additionalProperties: boolean;
@@ -211,6 +266,7 @@ export declare const promptToolDefinitions: ({
211
266
  export declare function getMessagePrompt(): Promise<PromptResponse>;
212
267
  export declare function listSubskillPrompts(limit?: number, includePublic?: boolean, includeInternal?: boolean): ListSubskillPromptsResponse;
213
268
  export declare function getSubskillPrompt(subskillName: string, offset?: number, limit?: number): SubskillPromptResponse;
269
+ export declare function getSubskillAsset(subskillName: string, assetPath: string, offset?: number, limit?: number): SubskillAssetResponse;
214
270
  export declare function completeSenderResearch(input?: CompleteSenderResearchInput): {
215
271
  readonly completedAt: string;
216
272
  readonly depth: "minimal-verification" | "deep-proof" | "parallel-batch";
@@ -1,5 +1,7 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
1
3
  import { getApi } from "../api.js";
2
- import { getSkillByName, listSkills, stripFrontmatter } from "../skills.js";
4
+ import { getSkillByName, listSkills, resolveSkillsDir, stripFrontmatter, } from "../skills.js";
3
5
  import { markCreateCampaignPromptLoaded, markResearchPromptLoaded, markSenderResearchCompleted, } from "./flow-preflight.js";
4
6
  // Chunk size sits below Claude Code's max-token cap on tool results
5
7
  // (empirically around ~50k chars — fetch_linkedin_profile breached at
@@ -71,6 +73,36 @@ export const promptToolDefinitions = [
71
73
  additionalProperties: false,
72
74
  },
73
75
  },
76
+ {
77
+ name: "get_subskill_asset",
78
+ description: "Load a packaged file that belongs to a Sellable subskill, such as core/flow.v2.json, core/auto-execute.yaml, or references/*.md. Use this instead of reading local repo files so Claude Code and Codex packaged runs share the same config.",
79
+ _meta: {
80
+ "anthropic/maxResultSizeChars": 200_000,
81
+ },
82
+ inputSchema: {
83
+ type: "object",
84
+ properties: {
85
+ subskillName: {
86
+ type: "string",
87
+ description: "Subskill name that owns the asset directory.",
88
+ },
89
+ assetPath: {
90
+ type: "string",
91
+ description: "Path relative to the subskill directory, for example core/flow.v2.json or references/sample-validation-loop.md.",
92
+ },
93
+ offset: {
94
+ type: "number",
95
+ description: "Optional character offset for chunked asset reads. Use for large reference files.",
96
+ },
97
+ limit: {
98
+ type: "number",
99
+ description: `Optional max characters to return for chunked asset reads. Defaults to ${DEFAULT_SUBSKILL_PROMPT_CHUNK_CHARS} for large assets and is capped at ${MAX_SUBSKILL_PROMPT_CHUNK_CHARS}.`,
100
+ },
101
+ },
102
+ required: ["subskillName", "assetPath"],
103
+ additionalProperties: false,
104
+ },
105
+ },
74
106
  {
75
107
  name: "search_subskill_prompts",
76
108
  description: "Search Sellable subskills by name/description. Use this before get_subskill_prompt if you are unsure which prompt to load.",
@@ -169,53 +201,104 @@ function markSubskillPromptLoaded(subskillName) {
169
201
  markResearchPromptLoaded("prospect", "research-prospect");
170
202
  }
171
203
  }
172
- export function getSubskillPrompt(subskillName, offset, limit) {
173
- const skill = getSkillByName(subskillName);
174
- if (!skill) {
175
- throw new Error(`Unknown subskill prompt: ${subskillName}`);
176
- }
177
- markSubskillPromptLoaded(subskillName);
178
- const fullPrompt = stripFrontmatter(skill.content);
179
- const safeOffset = Math.min(Math.max(Number.isFinite(offset) ? Math.floor(offset ?? 0) : 0, 0), fullPrompt.length);
204
+ function readChunkedContent(content, offset, limit, continuation) {
205
+ const safeOffset = Math.min(Math.max(Number.isFinite(offset) ? Math.floor(offset ?? 0) : 0, 0), content.length);
180
206
  const requestedLimit = Number.isFinite(limit) && limit !== undefined
181
207
  ? Math.max(Math.floor(limit), 1)
182
208
  : null;
183
- const defaultChunkLimit = fullPrompt.length > DEFAULT_SUBSKILL_PROMPT_CHUNK_CHARS
209
+ const defaultChunkLimit = content.length > DEFAULT_SUBSKILL_PROMPT_CHUNK_CHARS
184
210
  ? DEFAULT_SUBSKILL_PROMPT_CHUNK_CHARS
185
211
  : null;
186
212
  const safeLimit = requestedLimit === null
187
213
  ? defaultChunkLimit
188
214
  : Math.min(requestedLimit, MAX_SUBSKILL_PROMPT_CHUNK_CHARS);
189
- const prompt = safeLimit === null
190
- ? fullPrompt
191
- : fullPrompt.slice(safeOffset, safeOffset + safeLimit);
215
+ const chunk = safeLimit === null
216
+ ? content
217
+ : content.slice(safeOffset, safeOffset + safeLimit);
192
218
  const nextOffset = safeLimit === null
193
219
  ? null
194
- : Math.min(safeOffset + safeLimit, fullPrompt.length);
195
- const hasMore = nextOffset !== null && nextOffset < fullPrompt.length;
196
- const chunkCount = safeLimit === null
197
- ? 1
198
- : Math.max(Math.ceil(fullPrompt.length / safeLimit), 1);
220
+ : Math.min(safeOffset + safeLimit, content.length);
221
+ const hasMore = nextOffset !== null && nextOffset < content.length;
222
+ const chunkCount = safeLimit === null ? 1 : Math.max(Math.ceil(content.length / safeLimit), 1);
199
223
  const chunkIndex = safeLimit === null
200
224
  ? 1
201
225
  : Math.min(Math.floor(safeOffset / safeLimit) + 1, chunkCount);
202
226
  return {
203
- name: skill.name,
204
- description: skill.description,
205
- visibility: skill.visibility,
206
- prompt,
207
- promptLength: fullPrompt.length,
227
+ chunk,
228
+ contentLength: content.length,
208
229
  offset: safeOffset,
209
230
  limit: safeLimit ?? undefined,
210
231
  hasMore,
211
232
  nextOffset: hasMore ? nextOffset : null,
212
233
  chunkIndex,
213
234
  chunkCount,
214
- chunkingInstructions: hasMore
215
- ? `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.`
235
+ chunkingInstructions: hasMore && nextOffset !== null && safeLimit !== null && continuation
236
+ ? continuation(nextOffset, safeLimit)
216
237
  : undefined,
217
238
  };
218
239
  }
240
+ export function getSubskillPrompt(subskillName, offset, limit) {
241
+ const skill = getSkillByName(subskillName);
242
+ if (!skill) {
243
+ throw new Error(`Unknown subskill prompt: ${subskillName}`);
244
+ }
245
+ markSubskillPromptLoaded(subskillName);
246
+ const fullPrompt = stripFrontmatter(skill.content);
247
+ const chunked = readChunkedContent(fullPrompt, offset, limit, (nextOffset, safeLimit) => `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.`);
248
+ return {
249
+ name: skill.name,
250
+ description: skill.description,
251
+ visibility: skill.visibility,
252
+ prompt: chunked.chunk,
253
+ promptLength: chunked.contentLength,
254
+ offset: chunked.offset,
255
+ limit: chunked.limit,
256
+ hasMore: chunked.hasMore,
257
+ nextOffset: chunked.nextOffset,
258
+ chunkIndex: chunked.chunkIndex,
259
+ chunkCount: chunked.chunkCount,
260
+ chunkingInstructions: chunked.chunkingInstructions,
261
+ };
262
+ }
263
+ function resolveSubskillAssetPath(subskillName, assetPath) {
264
+ const cleanAssetPath = assetPath.replace(/\\/g, "/").trim();
265
+ if (cleanAssetPath === "" ||
266
+ cleanAssetPath.startsWith("/") ||
267
+ cleanAssetPath.split("/").includes("..")) {
268
+ throw new Error(`Invalid subskill asset path: ${assetPath}`);
269
+ }
270
+ const skillRoot = path.resolve(resolveSkillsDir(), subskillName);
271
+ const resolved = path.resolve(skillRoot, cleanAssetPath);
272
+ const relative = path.relative(skillRoot, resolved);
273
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
274
+ throw new Error(`Invalid subskill asset path: ${assetPath}`);
275
+ }
276
+ return { cleanAssetPath, resolved };
277
+ }
278
+ export function getSubskillAsset(subskillName, assetPath, offset, limit) {
279
+ if (!getSkillByName(subskillName)) {
280
+ throw new Error(`Unknown subskill prompt: ${subskillName}`);
281
+ }
282
+ const { cleanAssetPath, resolved } = resolveSubskillAssetPath(subskillName, assetPath);
283
+ if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) {
284
+ throw new Error(`Unknown subskill asset: ${subskillName}/${cleanAssetPath}`);
285
+ }
286
+ const content = fs.readFileSync(resolved, "utf8");
287
+ const chunked = readChunkedContent(content, offset, limit, (nextOffset, safeLimit) => `Continue this same asset load with get_subskill_asset({ subskillName: "${subskillName}", assetPath: "${cleanAssetPath}", offset: ${nextOffset}, limit: ${safeLimit} }) until hasMore is false. Treat all chunks as one asset load; do not use saved local tool-output files or machine-specific paths.`);
288
+ return {
289
+ subskillName,
290
+ assetPath: cleanAssetPath,
291
+ content: chunked.chunk,
292
+ contentLength: chunked.contentLength,
293
+ offset: chunked.offset,
294
+ limit: chunked.limit,
295
+ hasMore: chunked.hasMore,
296
+ nextOffset: chunked.nextOffset,
297
+ chunkIndex: chunked.chunkIndex,
298
+ chunkCount: chunked.chunkCount,
299
+ chunkingInstructions: chunked.chunkingInstructions,
300
+ };
301
+ }
219
302
  export function completeSenderResearch(input) {
220
303
  return markSenderResearchCompleted(input);
221
304
  }
@@ -34,6 +34,7 @@ type WaitForRubricResultsInput = {
34
34
  targetCount?: number;
35
35
  timeoutMs?: number;
36
36
  intervalMs?: number;
37
+ includeRows?: boolean;
37
38
  };
38
39
  type WorkflowTableStats = {
39
40
  tableId: string;
@@ -101,6 +102,7 @@ export declare const rubricToolDefinitions: ({
101
102
  targetCount?: undefined;
102
103
  timeoutMs?: undefined;
103
104
  intervalMs?: undefined;
105
+ includeRows?: undefined;
104
106
  };
105
107
  required: string[];
106
108
  additionalProperties: boolean;
@@ -132,6 +134,7 @@ export declare const rubricToolDefinitions: ({
132
134
  targetCount?: undefined;
133
135
  timeoutMs?: undefined;
134
136
  intervalMs?: undefined;
137
+ includeRows?: undefined;
135
138
  };
136
139
  required: string[];
137
140
  additionalProperties: boolean;
@@ -188,6 +191,7 @@ export declare const rubricToolDefinitions: ({
188
191
  targetCount?: undefined;
189
192
  timeoutMs?: undefined;
190
193
  intervalMs?: undefined;
194
+ includeRows?: undefined;
191
195
  };
192
196
  required: string[];
193
197
  additionalProperties: boolean;
@@ -244,6 +248,7 @@ export declare const rubricToolDefinitions: ({
244
248
  targetCount?: undefined;
245
249
  timeoutMs?: undefined;
246
250
  intervalMs?: undefined;
251
+ includeRows?: undefined;
247
252
  };
248
253
  required: string[];
249
254
  additionalProperties: boolean;
@@ -302,6 +307,7 @@ export declare const rubricToolDefinitions: ({
302
307
  targetCount?: undefined;
303
308
  timeoutMs?: undefined;
304
309
  intervalMs?: undefined;
310
+ includeRows?: undefined;
305
311
  };
306
312
  required: string[];
307
313
  additionalProperties: boolean;
@@ -330,6 +336,7 @@ export declare const rubricToolDefinitions: ({
330
336
  targetCount?: undefined;
331
337
  timeoutMs?: undefined;
332
338
  intervalMs?: undefined;
339
+ includeRows?: undefined;
333
340
  };
334
341
  required: string[];
335
342
  additionalProperties: boolean;
@@ -358,6 +365,7 @@ export declare const rubricToolDefinitions: ({
358
365
  targetCount?: undefined;
359
366
  timeoutMs?: undefined;
360
367
  intervalMs?: undefined;
368
+ includeRows?: undefined;
361
369
  };
362
370
  required: string[];
363
371
  additionalProperties: boolean;
@@ -388,6 +396,10 @@ export declare const rubricToolDefinitions: ({
388
396
  type: string;
389
397
  description: string;
390
398
  };
399
+ includeRows: {
400
+ type: string;
401
+ description: string;
402
+ };
391
403
  leadScoringRubrics?: undefined;
392
404
  requiredCheckNames?: undefined;
393
405
  rubric?: undefined;
@@ -466,6 +478,8 @@ export declare function checkRubric(input: CheckRubricInput): Promise<{
466
478
  tableId: string;
467
479
  }>;
468
480
  export declare function waitForRubricResults(input: WaitForRubricResultsInput): Promise<{
481
+ stats: WorkflowTableStats;
482
+ rows?: import("./rows.js").LightweightRow[] | undefined;
469
483
  ready: boolean;
470
484
  attempts: number;
471
485
  elapsedMs: number;
@@ -476,8 +490,6 @@ export declare function waitForRubricResults(input: WaitForRubricResultsInput):
476
490
  percent: number;
477
491
  targetCount: number;
478
492
  };
479
- rows: import("./rows.js").LightweightRow[];
480
- stats: WorkflowTableStats;
481
493
  reason?: undefined;
482
494
  } | {
483
495
  ready: boolean;
@@ -492,6 +504,5 @@ export declare function waitForRubricResults(input: WaitForRubricResultsInput):
492
504
  targetCount: number;
493
505
  };
494
506
  stats: WorkflowTableStats | null;
495
- rows?: undefined;
496
507
  }>;
497
508
  export {};
@@ -356,6 +356,10 @@ export const rubricToolDefinitions = [
356
356
  type: "number",
357
357
  description: `Polling interval in ms (default ${DEFAULT_INTERVAL_MS}).`,
358
358
  },
359
+ includeRows: {
360
+ type: "boolean",
361
+ description: "Whether to include minimal row snapshots in the response. Default true for backwards compatibility; pass false for stats-only polling in long autonomous flows.",
362
+ },
359
363
  },
360
364
  required: [],
361
365
  additionalProperties: false,
@@ -619,6 +623,7 @@ export async function waitForRubricResults(input) {
619
623
  const timeoutMs = Math.max(5000, input.timeoutMs ?? DEFAULT_TIMEOUT_MS);
620
624
  const intervalMs = Math.max(500, input.intervalMs ?? DEFAULT_INTERVAL_MS);
621
625
  const targetCount = resolveMaxProspects(input.targetCount);
626
+ const includeRows = input.includeRows !== false;
622
627
  let tableId = input.tableId;
623
628
  if (!tableId && input.campaignOfferId) {
624
629
  const campaign = await fetchCampaignOffer(input.campaignOfferId);
@@ -640,10 +645,11 @@ export async function waitForRubricResults(input) {
640
645
  if (completed >= effectiveTarget) {
641
646
  const passed = stats.passRate?.passed ?? 0;
642
647
  const percent = completed > 0 ? Math.round((passed / completed) * 100) : 0;
643
- // Fetch current row states so caller sees per-row enrich/ICP status
644
- const rowSnapshot = await getTableRowsMinimal(tableId, {
645
- limit: effectiveTarget,
646
- });
648
+ const rowSnapshot = includeRows
649
+ ? await getTableRowsMinimal(tableId, {
650
+ limit: effectiveTarget,
651
+ })
652
+ : null;
647
653
  return {
648
654
  ready: true,
649
655
  attempts,
@@ -655,7 +661,7 @@ export async function waitForRubricResults(input) {
655
661
  percent,
656
662
  targetCount: effectiveTarget,
657
663
  },
658
- rows: rowSnapshot.rows,
664
+ ...(rowSnapshot ? { rows: rowSnapshot.rows } : {}),
659
665
  stats,
660
666
  };
661
667
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.60",
3
+ "version": "0.1.62",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -8,6 +8,7 @@ allowed-tools:
8
8
  - mcp__sellable__wait_for_cli_login
9
9
  - mcp__sellable__bootstrap_create_campaign
10
10
  - mcp__sellable__get_subskill_prompt
11
+ - mcp__sellable__get_subskill_asset
11
12
  - mcp__sellable__search_subskill_prompts
12
13
  - mcp__sellable__get_provider_prompt
13
14
  - mcp__sellable__get_message_prompt
@@ -449,19 +450,24 @@ updates.
449
450
 
450
451
  1. Load canonical prompt via
451
452
  `mcp__sellable__get_subskill_prompt({ subskillName: "create-campaign-v2" })`.
452
- 2. Follow that prompt exactly.
453
- 3. For message generation, load the full `generate-messages` prompt in the
453
+ 2. Load the canonical workflow config via
454
+ `mcp__sellable__get_subskill_asset({ subskillName: "create-campaign-v2", assetPath: "core/flow.v2.json" })`.
455
+ Treat the returned JSON as the active state machine. Do not read repo-local
456
+ copies of this file; packaged Claude Code and Codex runs must use the MCP
457
+ asset loader so they share the same config.
458
+ 3. Follow that prompt and workflow config exactly.
459
+ 4. For message generation, load the full `generate-messages` prompt in the
454
460
  same run with chunked
455
461
  `mcp__sellable__get_subskill_prompt({ subskillName: "generate-messages", offset, limit })`
456
462
  calls until `hasMore` is false. Do not synthesize
457
463
  `message-validation.md` from the brief, lead review, or general knowledge.
458
- 4. Treat message quality as the gate before minting. Do not create a campaign,
464
+ 5. Treat message quality as the gate before minting. Do not create a campaign,
459
465
  show a commit gate, or mint anything until `message-validation.md` proves
460
466
  the full generate-messages workflow ran and `message-review.md` recommends
461
467
  `approve-message` against the gold-standard rules.
462
- 5. Do not create or mutate the live campaign until the approval gate returns
468
+ 6. Do not create or mutate the live campaign until the approval gate returns
463
469
  `approve`.
464
- 6. Do not ask the user to run another command.
470
+ 7. Do not ask the user to run another command.
465
471
 
466
472
  ## Fallback
467
473
 
@@ -74,6 +74,11 @@ Validated draft directory:
74
74
  - Net-new runs start at `bootstrap` -> `brief-interview` in
75
75
  `core/flow.v2.json`. Do not start at `validate-artifacts` unless resuming a
76
76
  compatibility run where all upstream artifacts already exist.
77
+ - Load `core/flow.v2.json` through
78
+ `get_subskill_asset({ subskillName: "create-campaign-v2", assetPath: "core/flow.v2.json" })`
79
+ at the start of the run and treat the returned JSON as the active state
80
+ machine. Do not read repo-local config files; packaged Claude Code and Codex
81
+ runs must use the MCP asset loader so they execute the same workflow.
77
82
  - Use the host-native structured question gate whenever it is exposed. In
78
83
  Claude Code, this is `AskUserQuestion`. In Codex, this is
79
84
  `request_user_input` (enabled in Default mode by
@@ -410,22 +415,23 @@ should test for this campaign. Those can run in parallel and usually take
410
415
  `approval-packet.md`.
411
416
  - Run the dependency chain as a DAG: `create-campaign-brief` -> `find leads`;
412
417
  once `lead-review.md` and `lead-sample.json` exist, run `filter leads` and
413
- `message generation` as parallel branches from the same basis (`brief.md`,
414
- `lead-review.md`, `lead-sample.json`). Approval waits for both
415
- `lead-filter.md` and `message-validation.md`, then reconciles that the
416
- selected message basis rows still pass the final filter.
417
- - Parallel means real parallel execution, not optimistic progress copy. After
418
- lead review, if the host exposes Task/subagent workers and host policy allows
419
- them for this user request, launch two disjoint workers: one owns
420
- `lead-filter.md` and optional `rubric.json`; the other owns the user-facing
421
- message generation branch and may write `message-prep.md`,
422
- `message-validation.md`, and `message-review.md`. If only parallel tool
423
- batching is available, batch independent tool reads/lookups only. If real
424
- parallel execution is not available or not allowed, run the same DAG
425
- sequentially and use honest copy: `I’ll tighten the filter first, then draft
426
- the message from the same sample.` Never say `kicking off two workstreams`,
427
- `in parallel`, or `background` unless parallel branches were actually
428
- launched.
418
+ `message generation` from the same basis (`brief.md`, `lead-review.md`,
419
+ `lead-sample.json`). Approval waits for both `lead-filter.md` and
420
+ `message-validation.md`, then reconciles that the selected message basis rows
421
+ still pass the final filter.
422
+ - Parallel means real parallel execution, not optimistic progress copy. Prefer
423
+ product-native parallelism that works in both Claude Code and Codex:
424
+ independent MCP/tool calls in the same model turn, or dedicated Sellable MCP
425
+ tools that perform their own server-side `Promise.all` fan-out. Host subagents
426
+ are optional acceleration only: use Claude Task/subagents or Codex subagents
427
+ only when the host exposes them, policy allows them, and the user explicitly
428
+ asked for agent fan-out. A normal `$sellable:create-campaign` invocation is
429
+ not an explicit request for Codex subagents. If only parallel tool batching is
430
+ available, batch independent tool reads/lookups only. If real parallel
431
+ execution is not available or not allowed, run the same DAG sequentially and
432
+ use honest copy: `I’ll tighten the filter first, then draft the message from
433
+ the same sample.` Never say `kicking off two workstreams`, `in parallel`, or
434
+ `background` unless parallel branches were actually launched.
429
435
  - Never run a downstream stage until the active `flow.v2.json` step's
430
436
  `requiredArtifacts` exist.
431
437
  - Never call a tool outside the active step's `allowedTools`, and never call a
@@ -524,17 +530,25 @@ lists as optional, not required. The three visible options are exactly:
524
530
 
525
531
  1. `Find people for me (recommended if you don't already have your own list)`
526
532
  2. `I have a CSV of LinkedIn profiles`
527
- 3. `I have a CSV of company domains`
533
+ 3. `I have a company/domain list to target`
534
+
535
+ Do not expose provider or implementation paths as the user's lead-source
536
+ choices. Never label the structured options as `engagers`, `titles`,
537
+ `Signals`, `Sales Nav`, `Prospeo`, `search LinkedIn posts`, or similar. Those
538
+ are internal scouting methods for the `Find people for me` path, not choices a
539
+ first-time user should have to understand.
528
540
 
529
541
  Keep `Other / custom` available for freeform answers such as a pasted list,
530
542
  an existing Sellable lead list, or another source idea. Do not put existing
531
543
  Sellable lead lists in the main three-option first batch; support them through
532
- custom/freeform input. If the user pastes up to 100 LinkedIn profile URLs or
533
- company domains, normalize the paste into a temporary local CSV and continue
534
- through the matching CSV preview path. Mixed, ambiguous, malformed, or oversized
535
- pastes should ask for a real CSV file instead of guessing. Uploaded CSV support
536
- is larger than paste support: LinkedIn profile CSVs can contain up to 7,500
537
- rows; domain CSVs can contain up to 7,500 rows but only 1,000 unique domains.
544
+ custom/freeform input. If the user chooses the company/domain option, ask in
545
+ normal chat whether they have a CSV, pasted domains, or pasted company names.
546
+ If the user pastes up to 100 LinkedIn profile URLs or company domains,
547
+ normalize the paste into a temporary local CSV and continue through the
548
+ matching CSV preview path. Mixed, ambiguous, malformed, or oversized pastes
549
+ should ask for a real CSV file instead of guessing. Uploaded CSV support is
550
+ larger than paste support: LinkedIn profile CSVs can contain up to 7,500 rows;
551
+ domain CSVs can contain up to 7,500 rows but only 1,000 unique domains.
538
552
 
539
553
  Avoid internal wording like `Which proof points should the message be allowed
540
554
  to lean on?` because it describes the artifact, not the founder decision.
@@ -573,7 +587,10 @@ Required behavior:
573
587
  - do not mutate DB-backed campaign state
574
588
  - run a real parallel source-angle scout when the source is not supplied and at
575
589
  least two viable source angles exist. Treat source scouting as independent
576
- branches, then compare the outputs in `lead-review.md`.
590
+ branches, then compare the outputs in `lead-review.md`. If the realistic
591
+ source set is Signals + Sales Nav, run both. If it is Signals + Prospeo, run
592
+ both. If all three are credible, run all three when the host/runtime supports
593
+ it.
577
594
  - Branch A: Signals / active LinkedIn posts. Search relevant keyword lanes,
578
595
  review finalist posts, fetch top-post engagers, and estimate warm-fit
579
596
  volume.
@@ -589,7 +606,7 @@ Required behavior:
589
606
  lanes and fetch top-post engagers in parallel when tool batching allows it.
590
607
  For Sales Nav-first or Prospeo/account-first campaigns, run multiple preview
591
608
  lanes for that provider in parallel and use Signals only as a warmth/quality
592
- check when relevant
609
+ check when relevant.
593
610
  - after every Sales Nav preview, validate that filters actually applied before
594
611
  using the lane in `lead-review.md`: `searchUrl` should include filters, the
595
612
  first page should match the intended roles/companies, and the result count
@@ -606,6 +623,16 @@ Required behavior:
606
623
  keyword lanes; it does not mean 492 prospects. The source decision must name
607
624
  the actual posts we would use, show why they won, and estimate usable engagers
608
625
  from those posts after headline/sample filtering
626
+ - Signals viability is based on estimated ICP-fit reachable engagers, not raw
627
+ post count and not whether it beats Sales Nav scale. If 5-8 selected posts can
628
+ plausibly produce ~150+ ICP-fit warm prospects before final filtering, keep
629
+ Signals as a viable focused option. If stricter cleanup likely reduces that to
630
+ ~100 usable prospects, say so plainly instead of discarding it.
631
+ - When Signals is viable-but-smaller and Sales Nav or Prospeo is more scalable,
632
+ the user-facing lead review must present the choice: Signals for a warmer,
633
+ smaller, higher-reply-upside first batch versus Sales Nav/Prospeo for broader
634
+ scale. Recommend the source you believe should win, often Sales Nav when scale
635
+ matters, but keep the smaller Signals lane available as a real option.
609
636
  - user-facing source logic must use numbers, not vibes. Never write a percentage
610
637
  such as `73% match` without the numerator, denominator, and sample basis, e.g.
611
638
  `8 of 11 sampled engagers fit the ICP (73%)`. If the sample is small, say it is
@@ -621,9 +648,10 @@ usable leads`.
621
648
  last 30 days, prefer the last 7-14 days when quality is comparable, and call
622
649
  out any older post as a deliberate tradeoff. Do not hide post age inside the
623
650
  raw search count
624
- - default source quality target is 300-500+ likely usable leads; accept
625
- 150-300 only for an explicit beta/design-partner motion and name that
626
- volume tradeoff in `lead-review.md` and the approval packet
651
+ - default source quality target is 300-500+ likely usable leads for scalable
652
+ outbound; accept ~150+ likely ICP-fit warm prospects as viable for a focused
653
+ Signals-first campaign or first review batch. Name the volume tradeoff in
654
+ `lead-review.md` and the approval packet rather than forcing a discard.
627
655
  - if `lead-source-intake.json` is present, read `sourceType`,
628
656
  `sourceInputMode`, file path or existing lead-list ID, selected columns,
629
657
  confirmation token, normalized counts, and any preview-created
@@ -64,6 +64,13 @@
64
64
  "requiredValues": {
65
65
  "subskillName": "create-campaign-v2"
66
66
  }
67
+ },
68
+ {
69
+ "tool": "get_subskill_asset",
70
+ "requiredValues": {
71
+ "subskillName": "create-campaign-v2",
72
+ "assetPath": "core/flow.v2.json"
73
+ }
67
74
  }
68
75
  ],
69
76
  "requiredArtifacts": [],
@@ -73,6 +80,7 @@
73
80
  "get_auth_status",
74
81
  "get_active_workspace",
75
82
  "get_subskill_prompt",
83
+ "get_subskill_asset",
76
84
  "AskUserQuestion",
77
85
  "request_user_input"
78
86
  ],
@@ -159,8 +167,17 @@
159
167
  "options": [
160
168
  "Find people for me (recommended if you don't already have your own list)",
161
169
  "I have a CSV of LinkedIn profiles",
162
- "I have a CSV of company domains"
170
+ "I have a company/domain list to target"
163
171
  ],
172
+ "forbiddenOptionLabels": [
173
+ "engagers",
174
+ "titles",
175
+ "Signals",
176
+ "Sales Nav",
177
+ "Prospeo",
178
+ "search LinkedIn posts"
179
+ ],
180
+ "forbiddenOptionLabelReason": "Provider/source mechanics are internal to the 'Find people for me' path; first-time users should choose the job they need done, not the scouting provider.",
164
181
  "customInput": true,
165
182
  "lastQuestionInFirstStrategyBatch": true
166
183
  }
@@ -169,6 +186,7 @@
169
186
  "producesArtifacts": ["brief-v1.md", "brief.md"],
170
187
  "allowedTools": [
171
188
  "get_subskill_prompt",
189
+ "get_subskill_asset",
172
190
  "get_auth_status",
173
191
  "get_active_workspace",
174
192
  "list_senders",
@@ -301,7 +319,7 @@
301
319
  "action": "run_subskill",
302
320
  "target": "find-leads",
303
321
  "mode": "campaignless-preview",
304
- "sourceScoutRule": "When source is not user-supplied and at least two viable source angles exist, scout independent source angles in parallel when host/tooling permits: Signals active-post branch, Sales Nav title/company branch, and Prospeo/domain branch only when relevant. If real parallel execution is unavailable, run the same scouts sequentially and do not claim parallel execution. Compare outputs by raw volume, n/N sampled fit, estimated good-fit range, expected reply range, and tradeoff."
322
+ "sourceScoutRule": "When source is not user-supplied and at least two viable source angles exist, scout independent source angles with product-native parallelism when host/tooling permits: independent MCP/tool calls in the same model turn or dedicated Sellable MCP tools that perform server-side fan-out. Signals active-post branch + Sales Nav title/company branch, Signals + Prospeo when domains/accounts are relevant, or all three when credible. Do not rely on Codex subagents for a normal $sellable:create-campaign invocation; use host subagents only when the user explicitly asked for agent fan-out. If real parallel execution is unavailable, run the same scouts sequentially and do not claim parallel execution. Compare outputs by raw volume, n/N sampled fit, estimated good-fit range, expected reply range, and tradeoff. Keep Signals as viable when selected posts can produce ~150+ ICP-fit warm prospects before final filtering, even if Sales Nav is more scalable; when both are viable, present the choice and recommend the stronger default."
305
323
  },
306
324
  {
307
325
  "action": "write_artifacts",
@@ -312,6 +330,7 @@
312
330
  "producesArtifacts": ["lead-review.md", "lead-sample.json"],
313
331
  "allowedTools": [
314
332
  "get_subskill_prompt",
333
+ "get_subskill_asset",
315
334
  "get_provider_prompt",
316
335
  "lookup_sales_nav_filter",
317
336
  "search_sales_nav",
@@ -415,7 +434,7 @@
415
434
  "parallel only if real parallel branches were launched"
416
435
  ],
417
436
  "timeEstimate": "~2-3 min",
418
- "chatRenderRule": "If real parallel workers/branches were actually launched, say: 'I’m kicking off two workstreams now' and list 'Tighten the fit filter' and 'Message generation'. If not, do not mention parallel/background work; say: 'I’ll tighten the filter first, then run message generation from the same approved brief and sample leads.' Never claim parallelism unless the host actually started parallel execution. User-facing stage name is message generation; message-validation.md is only the internal artifact."
437
+ "chatRenderRule": "If real parallel MCP/tool branches or explicitly requested host subagents were actually launched, say: 'I’m kicking off two workstreams now' and list 'Tighten the fit filter' and 'Message generation'. If not, do not mention parallel/background work; say: 'I’ll tighten the filter first, then run message generation from the same approved brief and sample leads.' Never claim parallelism unless parallel execution actually started. A normal $sellable:create-campaign invocation is not an explicit request for Codex subagents. User-facing stage name is message generation; message-validation.md is only the internal artifact."
419
438
  },
420
439
  {
421
440
  "action": "ask_continue_revise_or_confirm_only_if_needed",
@@ -503,6 +522,7 @@
503
522
  "optionalProducesArtifacts": ["rubric.json"],
504
523
  "allowedTools": [
505
524
  "get_subskill_prompt",
525
+ "get_subskill_asset",
506
526
  "AskUserQuestion",
507
527
  "request_user_input"
508
528
  ],
@@ -554,6 +574,7 @@
554
574
  "producesArtifacts": ["message-validation.md"],
555
575
  "allowedTools": [
556
576
  "get_subskill_prompt",
577
+ "get_subskill_asset",
557
578
  "AskUserQuestion",
558
579
  "request_user_input"
559
580
  ],
@@ -1015,6 +1036,11 @@
1015
1036
  {
1016
1037
  "action": "load_auto_execute_config",
1017
1038
  "source": "core/auto-execute.yaml",
1039
+ "tool": "get_subskill_asset",
1040
+ "requiredValues": {
1041
+ "subskillName": "create-campaign-v2",
1042
+ "assetPath": "core/auto-execute.yaml"
1043
+ },
1018
1044
  "loadOnce": true
1019
1045
  },
1020
1046
  {
@@ -1022,7 +1048,11 @@
1022
1048
  "target": "85-02"
1023
1049
  }
1024
1050
  ],
1025
- "allowedTools": ["AskUserQuestion", "request_user_input"],
1051
+ "allowedTools": [
1052
+ "get_subskill_asset",
1053
+ "AskUserQuestion",
1054
+ "request_user_input"
1055
+ ],
1026
1056
  "transitions": {
1027
1057
  "autonomous_tail_started": "auto-execute-leads"
1028
1058
  }
@@ -1105,6 +1135,7 @@
1105
1135
  }
1106
1136
  ],
1107
1137
  "allowedTools": [
1138
+ "get_subskill_asset",
1108
1139
  "import_leads",
1109
1140
  "wait_for_lead_list_ready",
1110
1141
  "confirm_lead_list",
@@ -1153,11 +1184,14 @@
1153
1184
  "tool": "wait_for_rubric_results",
1154
1185
  "requiredFields": ["targetCount"],
1155
1186
  "targetCountSource": "stats.totalRows_or_imported_batch_count",
1187
+ "requiredValues": {
1188
+ "includeRows": false
1189
+ },
1156
1190
  "note": "default targetCount=25 returns ready=true after first 25 completions even when cohort is still pending; always pass cohortSize explicitly (see references/sample-validation-loop.md §Known Tool Behaviors #3)",
1157
- "readVia": "subagent",
1191
+ "readVia": "stats_only_tool_result",
1158
1192
  "extractFields": ["ready", "passRate.completed", "stats"],
1159
1193
  "doNotRetain": "rows_payload",
1160
- "contextBloatNote": "25 rows ≈ 67KB; 100 rows ≈ 268KB; use subagent extraction (see references/sample-validation-loop.md §Known Tool Behaviors #2)"
1194
+ "contextBloatNote": "Pass includeRows=false so the MCP tool returns stats only; 25 rows ≈ 67KB and 100 rows ≈ 268KB when row payloads are retained (see references/sample-validation-loop.md §Known Tool Behaviors #2)."
1161
1195
  },
1162
1196
  {
1163
1197
  "action": "compute_projected_pass",
@@ -1176,6 +1210,7 @@
1176
1210
  }
1177
1211
  ],
1178
1212
  "allowedTools": [
1213
+ "get_subskill_asset",
1179
1214
  "get_rows_minimal",
1180
1215
  "wait_for_campaign_table_ready",
1181
1216
  "wait_for_rubric_results",
@@ -1287,6 +1322,7 @@
1287
1322
  }
1288
1323
  ],
1289
1324
  "allowedTools": [
1325
+ "get_subskill_asset",
1290
1326
  "get_rows_minimal",
1291
1327
  "queue_cells",
1292
1328
  "wait_for_campaign_table_ready",
@@ -1345,6 +1381,7 @@
1345
1381
  }
1346
1382
  ],
1347
1383
  "allowedTools": [
1384
+ "get_subskill_asset",
1348
1385
  "attach_recommended_sequence",
1349
1386
  "AskUserQuestion",
1350
1387
  "request_user_input"
@@ -56,7 +56,7 @@ auto-revise leads.
56
56
  accidentally stop early
57
57
  (see §Known Tool Behaviors #3)
58
58
 
59
- 7. read the result via a subagent; extract ONLY:
59
+ 7. call `wait_for_rubric_results` with `includeRows=false`; extract ONLY:
60
60
  - ready: boolean
61
61
  - passRate.completed: number
62
62
  - stats: object
@@ -110,16 +110,11 @@ Observed: default response includes `carryData.Post Content` with
110
110
  whole LinkedIn post bodies. ~67KB per 25 rows. ~268KB per 100 rows.
111
111
  Retaining the full payload across tail turns blows Opus context.
112
112
 
113
- Workaround (short-term): invoke `wait_for_rubric_results` inside a
114
- subagent turn and extract ONLY `ready`, `passRate.completed`, and
115
- `stats`. Never put the raw rows payload back into the tail's own
116
- context. Prefer `get_rows_minimal` (not `get_rows`) when row-level
113
+ Workaround: pass `includeRows=false` so `wait_for_rubric_results`
114
+ returns stats only. Never put raw rows payload back into the tail's
115
+ own context. Prefer `get_rows_minimal` (not `get_rows`) when row-level
117
116
  inspection is required, and strip `carryData` before retention.
118
117
 
119
- Workaround (long-term): add a `includeRows=false` / stats-only mode
120
- to `wait_for_rubric_results` at the MCP tool level. Tracked as a
121
- backlog item; until then the subagent-read pattern is the contract.
122
-
123
118
  ### 3. `wait_for_rubric_results.targetCount` defaults to 25
124
119
 
125
120
  Observed: default `targetCount=25` returns `ready=true` after the first 25
@@ -104,8 +104,11 @@ Message` column's http_request writes those cells via the cascade.
104
104
  generation is a credit-spend decision and must happen after the user
105
105
  has reviewed the sample.
106
106
 
107
- Load `core/auto-execute.yaml` exactly once at the start of Step 13. All
108
- subsequent steps read the already-parsed config. Do not re-load mid-run.
107
+ Load `core/auto-execute.yaml` exactly once at the start of Step 13 through
108
+ `get_subskill_asset({ subskillName: "create-campaign-v2", assetPath: "core/auto-execute.yaml" })`.
109
+ All subsequent steps read the already-parsed config. Do not re-load mid-run,
110
+ and do not read repo-local config files; packaged Claude Code and Codex runs
111
+ must use the MCP asset loader.
109
112
  Load each subskill prompt (`create-campaign-v2`, `research-sender`,
110
113
  `generate-messages`) at most once per run. A multi-call chunk sequence counts
111
114
  as one load. If a tool result already told you to load a prompt, load all
@@ -155,8 +158,9 @@ atomic mint).
155
158
  > for the lead-list table by name or skip the `mode: "add"` path; do
156
159
  > not pass the campaign-table id as `sourceLeadListId`.
157
160
 
158
- 1. Load `core/auto-execute.yaml`. Capture `import.importLimit`,
159
- `sample.sampleSize`, `sample.minProjectedPass`,
161
+ 1. Load `core/auto-execute.yaml` through
162
+ `get_subskill_asset({ subskillName: "create-campaign-v2", assetPath: "core/auto-execute.yaml" })`.
163
+ Capture `import.importLimit`, `sample.sampleSize`, `sample.minProjectedPass`,
160
164
  `sample.maxRevisionRounds`, `messaging.tokenContract`,
161
165
  `messaging.critique.enabled`, `handoff.autoStart`,
162
166
  `handoff.orientation`, `retry.sameToolSameError`,
@@ -443,4 +447,12 @@ runs.
443
447
  | `core/auto-execute.yaml` | Start of Step 13, load once; all tail steps read parsed values |
444
448
  | `core/auto-execute.README.md` | When tuning `auto-execute.yaml` knobs or reviewing threshold-trip logs |
445
449
 
450
+ Load every file in this table with:
451
+
452
+ ```text
453
+ get_subskill_asset({ subskillName: "create-campaign-v2", assetPath: "<file>" })
454
+ ```
455
+
456
+ Do not use local filesystem reads for these files in customer runs.
457
+
446
458
  </references_index>
@@ -235,6 +235,11 @@ Use the existing provider tools to run a small, directional probe:
235
235
 
236
236
  If the first probe is weak or noisy and enough context exists, try 1-2 alternate hypotheses before returning.
237
237
  If the first probe has good quality but insufficient scale, iterate 1-2 times to widen intelligently before returning.
238
+ When two or more source angles are viable, run the provider probes in real
239
+ parallel when the host/runtime allows it. Examples: Signals + Sales Nav,
240
+ Signals + Prospeo, or Sales Nav + Prospeo. If the host cannot run them in
241
+ parallel, run the same probes sequentially and do not claim they ran in
242
+ parallel.
238
243
 
239
244
  Treat refinement as a measured loop:
240
245
 
@@ -262,10 +267,23 @@ Use first when the value is in conversation opportunity, competitor engagers, co
262
267
  - Favor ICP-fit conversation density over raw post volume.
263
268
  - Capture counts, false-positive patterns, and notable examples.
264
269
  - If Signals is materially in the running, do not stop at post-level quality alone.
265
- - Pick 1-2 promising posts, then call `fetch_post_engagers` on those posts.
270
+ - Pick enough promising posts to estimate campaign viability. Start with 1-3
271
+ strong posts for a spot check; if the sampled pass rate is good but the
272
+ projected pool is below target, expand to 5-8 selected posts before
273
+ discarding Signals.
266
274
  - Sample a representative first page only, scoring the first 25-40 engagers across the chosen posts against a rough yes/no headline rubric or `headlineICPCriteria`.
267
275
  - Use headline and display-name cues only for the spot-check sample; do not enrich people during this phase.
268
276
  - Base `estimatedReachableLeads` on the sampled engager pass rate, not only on a guessed discount from post themes.
277
+ - A Signals lane with ~150+ estimated ICP-fit reachable engagers from selected
278
+ posts is viable for a focused warm campaign, even if Sales Nav is more
279
+ scalable. Do not discard that lane solely for being smaller. If post-level
280
+ filtering likely reduces 150 good fits to ~100 after stricter cleanup, say so
281
+ plainly and treat it as a viable-but-smaller option.
282
+ - When Signals and Sales Nav/Prospeo are both viable, present the tradeoff and
283
+ ask for the user's source preference if the decision changes campaign
284
+ strategy: Signals = warmer, smaller, higher reply upside; Sales Nav/Prospeo =
285
+ more scalable, usually colder or less signal-rich. Recommend the source you
286
+ believe is better, but keep the viable alternate as a real option.
269
287
  - If you cannot fetch engagers in the current runtime, say the estimate is inferred and lower confidence.
270
288
  - If Signals is too sparse, too noisy, or clearly below campaign scale after the spot check, fall back to `Sales Nav` rather than forcing a weak conversation-led lane.
271
289
 
@@ -2263,21 +2263,27 @@ borrow the closest message shape from
2263
2263
 
2264
2264
  When crafting messages for multiple rows (e.g., "craft rows 1-5"):
2265
2265
 
2266
- **CRITICAL: Use parallel subagents for efficiency.**
2266
+ **CRITICAL: Use real parallelism only when the host explicitly supports it for
2267
+ this run. Prefer product-native MCP/tool parallelism that works in both Claude
2268
+ Code and Codex; host subagents are optional acceleration only when the user
2269
+ explicitly asked for agent fan-out.**
2267
2270
 
2268
2271
  1. Load campaign examples, calibration notes, and bounded copy from the brief first.
2269
2272
  2. Call `get_message_prompt()` once to get the fallback REPLY framework and QA checks
2270
- 3. Spawn parallel subagents using the `Task` tool with explicit `model: "opus"` and `subagent_type: "general-purpose"` (never omit model):
2273
+ 3. If the user explicitly asked for agent fan-out and the host exposes subagents,
2274
+ spawn parallel workers with explicit model/config. Otherwise batch independent
2275
+ MCP/tool reads where available or run rows sequentially with honest progress
2276
+ copy. Do not claim subagents or background work unless they actually started.
2271
2277
 
2272
2278
  ```
2273
- For each row (1-5), launch a Task subagent:
2279
+ For each row (1-5), launch a worker only when allowed:
2274
2280
  - subagent_type: "general-purpose"
2275
2281
  - model: "opus"
2276
2282
  - prompt: Include campaign context, sender info, row data, gold-standard message examples when present, and fallback REPLY framework
2277
2283
  - Do NOT use run_in_background (avoids notification spam)
2278
2284
  ```
2279
2285
 
2280
- 4. Each subagent:
2286
+ 4. Each worker or sequential row pass:
2281
2287
 
2282
2288
  - Researches the prospect (WebSearch, LinkedIn tools)
2283
2289
  - Crafts message by following the campaign's gold-standard pattern first
@@ -2285,7 +2291,7 @@ For each row (1-5), launch a Task subagent:
2285
2291
  - Uses the fallback REPLY framework only as structure and QA support
2286
2292
  - Returns the draft + research notes (do NOT save)
2287
2293
 
2288
- 5. Wait for all subagents to complete
2294
+ 5. Wait for all workers to complete, or finish the sequential row pass
2289
2295
  6. Auto-save drafts for rows where the message cell is empty (announce "Draft saved (not approved)")
2290
2296
  7. Show summary:
2291
2297
 
@@ -2303,9 +2309,9 @@ Approve all for sending?
2303
2309
  - Review individually first
2304
2310
  ```
2305
2311
 
2306
- ### Subagent Prompt Template
2312
+ ### Optional Worker Prompt Template
2307
2313
 
2308
- When spawning craft subagents, include:
2314
+ When spawning craft workers, include:
2309
2315
 
2310
2316
  ```
2311
2317
  Craft a personalized LinkedIn message for this prospect using the campaign's
@@ -2371,7 +2377,9 @@ research → condense → 10 angle agents in parallel → finalizer combines bes
2371
2377
  - use the strongest safe proof
2372
2378
  - make the CTA a useful next step, not a vague meeting ask
2373
2379
  5. Build a condensed angle brief (~1.5k tokens) from research + sender info
2374
- 6. Fan out 10 angle agents (model: "sonnet", run_in_background: true):
2380
+ 6. If explicit host subagent fan-out is available, fan out 10 angle workers
2381
+ (model: "sonnet", no delayed/background notifications). Otherwise explore
2382
+ a smaller bounded set of angles sequentially and do not claim parallelism:
2375
2383
  - Each gets the condensed brief + their angle instruction (~2k tokens per agent)
2376
2384
  - DO NOT pass the full fallback REPLY framework to angle agents, only the brief
2377
2385
  7. After all 10 return, act as the Finalizer:
@@ -2384,7 +2392,7 @@ research → condense → 10 angle agents in parallel → finalizer combines bes
2384
2392
  8. Return: { success: true, message: "...", research: [...], anglesExplored: 10 }
2385
2393
  ```
2386
2394
 
2387
- **IMPORTANT:** Load `.sellable/configs/writing/outbound.md`, `styleguide-core.md`, and `.sellable/insights/cross-skill.md` ONCE in the lead agent, then pass **condensed** versions to subagents. Don't pass full file contents to every subagent — that wastes context and causes overflow.
2395
+ **IMPORTANT:** Load `.sellable/configs/writing/outbound.md`, `styleguide-core.md`, and `.sellable/insights/cross-skill.md` ONCE in the lead agent, then pass **condensed** versions to any workers. Don't pass full file contents to every worker — that wastes context and causes overflow.
2388
2396
 
2389
2397
  ## Batch Approval
2390
2398
 
@@ -33,9 +33,12 @@ Provide as many as available. Use `"Unknown"` when missing.
33
33
 
34
34
  Choose backend once:
35
35
 
36
- 1. If `Task` is available: use Task subagents in parallel.
37
- 2. Else if `multi_tool_use.parallel` is available: batch independent tool calls via `multi_tool_use.parallel`.
38
- 3. Else: run units sequentially.
36
+ 1. If product-native MCP/tool parallel calls are available: batch independent
37
+ retrieval units in one turn.
38
+ 2. If the user explicitly asked for agent fan-out and `Task` is available: use
39
+ Task subagents in parallel.
40
+ 3. Else if `multi_tool_use.parallel` is available: batch independent tool calls via `multi_tool_use.parallel`.
41
+ 4. Else: run units sequentially.
39
42
 
40
43
  Never claim Task subagents were used when they were not.
41
44