@sellable/mcp 0.1.61 → 0.1.63

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.61",
3
+ "version": "0.1.63",
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
@@ -523,18 +529,26 @@ batch, after buyer, offer/ask, and proof/safety are understood. Frame supplied
523
529
  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
- 2. `I have a CSV of LinkedIn profiles`
527
- 3. `I have a CSV of company domains`
532
+ 2. `I have a CSV of profiles to reach out to`
533
+ 3. `I have a CSV of companies/domains I want 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.
@@ -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
  ],
@@ -158,9 +166,18 @@
158
166
  "question": "How should we get the people for this campaign?",
159
167
  "options": [
160
168
  "Find people for me (recommended if you don't already have your own list)",
161
- "I have a CSV of LinkedIn profiles",
162
- "I have a CSV of company domains"
169
+ "I have a CSV of profiles to reach out to",
170
+ "I have a CSV of companies/domains I want 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 real parallel when host/tooling permits: Signals active-post branch + Sales Nav title/company branch, Signals + Prospeo when domains/accounts are relevant, or all three when credible. 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."
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>
@@ -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