@sellable/mcp 0.1.315 → 0.1.319

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.
@@ -20,6 +20,23 @@ function normalizeLeadSourceProvider(provider) {
20
20
  return "csv-linkedin";
21
21
  return undefined;
22
22
  }
23
+ function isPositiveNumber(value) {
24
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
25
+ }
26
+ function hasSignalDiscoveryProgressEvidence(rowCount, importProgress) {
27
+ const phase = importProgress?.phase ?? null;
28
+ return (rowCount > 0 ||
29
+ isPositiveNumber(importProgress?.processed) ||
30
+ isPositiveNumber(importProgress?.leadsImported) ||
31
+ isPositiveNumber(importProgress?.scrapedEngagers) ||
32
+ isPositiveNumber(importProgress?.evaluatedEngagers) ||
33
+ isPositiveNumber(importProgress?.evaluationTotal) ||
34
+ isPositiveNumber(importProgress?.activeEvaluationBatches) ||
35
+ isPositiveNumber(importProgress?.queuedForEvaluation) ||
36
+ isPositiveNumber(importProgress?.postsScraped) ||
37
+ phase === "scraping" ||
38
+ phase === "processing");
39
+ }
23
40
  function sleep(ms) {
24
41
  return new Promise((resolve) => setTimeout(resolve, ms));
25
42
  }
@@ -233,6 +250,8 @@ export async function waitForLeadListReady(input) {
233
250
  if (typeof configTarget === "number" && !targetLeadCount) {
234
251
  targetLeadCount = configTarget;
235
252
  }
253
+ const signalDiscoveryHasProgressEvidence = provider === "signal-discovery" &&
254
+ hasSignalDiscoveryProgressEvidence(rowCount, importProgress);
236
255
  let jobId = input.jobId;
237
256
  if (!jobId && typeof config?.importJobId === "string") {
238
257
  jobId = config.importJobId;
@@ -253,7 +272,9 @@ export async function waitForLeadListReady(input) {
253
272
  targetLeadCount,
254
273
  };
255
274
  }
256
- if (configError && configStatus !== "complete") {
275
+ if (configError &&
276
+ configStatus !== "complete" &&
277
+ !signalDiscoveryHasProgressEvidence) {
257
278
  return {
258
279
  ready: false,
259
280
  reason: "import_failed",
@@ -267,21 +288,31 @@ export async function waitForLeadListReady(input) {
267
288
  error: configError,
268
289
  };
269
290
  }
291
+ if (configError &&
292
+ configStatus !== "complete" &&
293
+ signalDiscoveryHasProgressEvidence) {
294
+ lastError = configError;
295
+ }
270
296
  if (configStatus) {
271
297
  lastStatus = configStatus;
272
298
  if (configStatus === "error") {
273
- return {
274
- ready: false,
275
- reason: "import_failed",
276
- leadListId,
277
- provider: provider ?? null,
278
- attempts,
279
- elapsedMs: Date.now() - start,
280
- rowCount,
281
- status: configStatus,
282
- targetLeadCount,
283
- error: configError,
284
- };
299
+ if (signalDiscoveryHasProgressEvidence) {
300
+ lastError = configError ?? lastError;
301
+ }
302
+ else {
303
+ return {
304
+ ready: false,
305
+ reason: "import_failed",
306
+ leadListId,
307
+ provider: provider ?? null,
308
+ attempts,
309
+ elapsedMs: Date.now() - start,
310
+ rowCount,
311
+ status: configStatus,
312
+ targetLeadCount,
313
+ error: configError,
314
+ };
315
+ }
285
316
  }
286
317
  if (configStatus === "complete") {
287
318
  importComplete = true;
@@ -407,6 +407,10 @@ export declare const allTools: ({
407
407
  type: string;
408
408
  description: string;
409
409
  };
410
+ modelMetadataSource: {
411
+ type: string;
412
+ description: string;
413
+ };
410
414
  };
411
415
  additionalProperties: boolean;
412
416
  };
@@ -6309,6 +6313,7 @@ export declare const allTools: ({
6309
6313
  postUrl?: undefined;
6310
6314
  sources?: undefined;
6311
6315
  full?: undefined;
6316
+ fetchMinimal?: undefined;
6312
6317
  companyUrl?: undefined;
6313
6318
  sortBy?: undefined;
6314
6319
  linkedin_url?: undefined;
@@ -6339,6 +6344,7 @@ export declare const allTools: ({
6339
6344
  };
6340
6345
  linkedinUrl?: undefined;
6341
6346
  full?: undefined;
6347
+ fetchMinimal?: undefined;
6342
6348
  companyUrl?: undefined;
6343
6349
  sortBy?: undefined;
6344
6350
  linkedin_url?: undefined;
@@ -6361,6 +6367,11 @@ export declare const allTools: ({
6361
6367
  description: string;
6362
6368
  default: boolean;
6363
6369
  };
6370
+ fetchMinimal: {
6371
+ type: string;
6372
+ description: string;
6373
+ default: boolean;
6374
+ };
6364
6375
  limit?: undefined;
6365
6376
  postUrl?: undefined;
6366
6377
  sources?: undefined;
@@ -6386,6 +6397,7 @@ export declare const allTools: ({
6386
6397
  postUrl?: undefined;
6387
6398
  sources?: undefined;
6388
6399
  full?: undefined;
6400
+ fetchMinimal?: undefined;
6389
6401
  sortBy?: undefined;
6390
6402
  linkedin_url?: undefined;
6391
6403
  max_posts?: undefined;
@@ -6417,6 +6429,7 @@ export declare const allTools: ({
6417
6429
  postUrl?: undefined;
6418
6430
  sources?: undefined;
6419
6431
  full?: undefined;
6432
+ fetchMinimal?: undefined;
6420
6433
  linkedin_url?: undefined;
6421
6434
  max_posts?: undefined;
6422
6435
  };
@@ -6442,6 +6455,7 @@ export declare const allTools: ({
6442
6455
  postUrl?: undefined;
6443
6456
  sources?: undefined;
6444
6457
  full?: undefined;
6458
+ fetchMinimal?: undefined;
6445
6459
  companyUrl?: undefined;
6446
6460
  sortBy?: undefined;
6447
6461
  };
@@ -6462,6 +6476,7 @@ export declare const allTools: ({
6462
6476
  postUrl?: undefined;
6463
6477
  sources?: undefined;
6464
6478
  full?: undefined;
6479
+ fetchMinimal?: undefined;
6465
6480
  companyUrl?: undefined;
6466
6481
  sortBy?: undefined;
6467
6482
  max_posts?: undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.315",
3
+ "version": "0.1.319",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -111,6 +111,10 @@ Blueprint rules:
111
111
  - Use stable, readable column ids such as `linkedin_url`, `enrich_prospect`, and
112
112
  `score_icp_mcp`.
113
113
  - Use registry type keys exactly, not display names.
114
+ - Treat the API column registry as the source of truth for type validation.
115
+ MCP code must not maintain its own hardcoded column-type allowlist; if the API
116
+ returns `phantom_type`, `deprecated_type`, or `not_createable_type`, fix the
117
+ blueprint to match the current registry response.
114
118
  - Treat "Enrich Prospect" as a `http_request` preset, not its own type.
115
119
  - Use `score_icp_mcp` for new AI ICP scoring. Do not use legacy
116
120
  `score_icp` or the old `score_icp_rubric` alias; they are reserved for older
@@ -145,6 +149,28 @@ Blueprint rules:
145
149
  synced sender connections, a positive `check_connection`, or accepted invite
146
150
  proof from the current flow.
147
151
 
152
+ Config rules:
153
+
154
+ - Put native column settings in `config`. The commit path preserves full config
155
+ objects and validates them server-side before mutation.
156
+ - Use blueprint-level `inputMapping` for references to existing producer
157
+ columns. The commit path materializes those references into production
158
+ template mappings such as `{{<actual column id>}}` or
159
+ `{{<actual Enrich Prospect column id>.id}}`.
160
+ - Use `runCondition` for branch gates that should exist in production. Template
161
+ tokens in `config`, AI prompts, formula expressions, and `runCondition` are
162
+ dependency-bearing and must point to real producer columns.
163
+ - For outbound HTTP throttling, set `http_request.config.rateLimit` as either
164
+ `{ "mode": "window", "maxRequests": N, "windowSeconds": S }` or
165
+ `{ "mode": "concurrency", "maxConcurrent": N }`.
166
+ - For public webhook intake throttling, set
167
+ `inbound_webhook.config.rateLimit` as
168
+ `{ "maxRequests": N, "windowSeconds": S }`. Omit it to use the server default.
169
+ - Do not invent per-column `rateLimit` for LinkedIn action columns. Those are
170
+ governed by sender/account limits and scheduling windows; configure action
171
+ behavior with `actionConfig`, `waitForEvent`, `runCondition`, and producer
172
+ mappings.
173
+
148
174
  ## Commit phase
149
175
 
150
176
  Use exactly one tool for the initial commit:
@@ -55,10 +55,11 @@ read/edit/delete safety, but they are not valid `add_column` or
55
55
 
56
56
  - type: `datetime`
57
57
  - displayName: `Date/Time`
58
+ - lifecycle: hidden active, load-only; not a valid create target.
58
59
  - category: Lead Data
59
60
  - inputs: none.
60
61
  - outputs: ISO-like date/time value.
61
- - when to use: Use for system-owned scheduling timestamps.
62
+ - when to use: Existing tables may contain system-owned scheduling timestamps.
62
63
  - when NOT to use: Do not use to wait; use `wait` for sequence timing.
63
64
 
64
65
  ### Next Action
@@ -117,6 +118,8 @@ read/edit/delete safety, but they are not valid `add_column` or
117
118
  - displayName: `Inbound Webhook`
118
119
  - category: Lead Data
119
120
  - inputs: none.
121
+ - config: optional `rateLimit` as `{ "maxRequests": N, "windowSeconds": S }`;
122
+ server defaults to 100 requests per 60 seconds when omitted.
120
123
  - outputs: received JSON payload and webhook metadata.
121
124
  - when to use: Use when rows are populated by external webhook events.
122
125
  - when NOT to use: Do not use for polling APIs; use `http_request`.
@@ -163,6 +166,8 @@ read/edit/delete safety, but they are not valid `add_column` or
163
166
  - displayName: `Check Open Profile`
164
167
  - category: LinkedIn Actions
165
168
  - inputs: required `linkedin_url`.
169
+ - config: may use the same `rateLimit` shape as `http_request` for provider
170
+ lookup throttling.
166
171
  - outputs: boolean open-profile decision and lookup metadata.
167
172
  - when to use: Use before selecting open-profile InMail behavior.
168
173
  - when NOT to use: Do not use to check first-degree connection; use `check_connection`.
@@ -296,6 +301,11 @@ read/edit/delete safety, but they are not valid `add_column` or
296
301
 
297
302
  ## Data & Logic
298
303
 
304
+ LinkedIn action columns do not accept a generic per-column `rateLimit`. They
305
+ use sender/account daily limits, sending hours, and action scheduling outside
306
+ the blueprint column config. Use `actionConfig.waitForEvent`, `runCondition`,
307
+ and explicit producer mappings for action behavior.
308
+
299
309
  ### AI Column
300
310
 
301
311
  - type: `ai_column`
@@ -332,6 +342,10 @@ read/edit/delete safety, but they are not valid `add_column` or
332
342
  - displayName: `HTTP Request`
333
343
  - category: Data & Logic
334
344
  - inputs: required `method`, required `endpoint`, optional `headers`, optional `body`.
345
+ - config: optional `rateLimit` supports rolling windows
346
+ `{ "mode": "window", "maxRequests": N, "windowSeconds": S }` and concurrency
347
+ lanes `{ "mode": "concurrency", "maxConcurrent": N }`. Omit it to use the
348
+ server's default I/O concurrency lane.
335
349
  - outputs: HTTP status, response body, and metadata; Enrich Prospect also returns root `id` as the EnrichedProspect id.
336
350
  - when to use: Use for REST API calls, including the canonical "Enrich Prospect" preset at `/api/v4/enrich-prospect`.
337
351
  - when NOT to use: Do not use when a first-party typed column already handles the action.
@@ -31,6 +31,7 @@ const httpEnrichConfig = {
31
31
  endpoint: { value: "/api/v4/enrich-prospect" },
32
32
  body: { value: '{"linkedinUrl":"{{LinkedIn URL}}"}' },
33
33
  },
34
+ runCondition: "{{LinkedIn URL}} !== ''",
34
35
  };
35
36
 
36
37
  export const COMMON_BLUEPRINTS: CommonBlueprintEntry[] = [
@@ -51,7 +52,10 @@ export const COMMON_BLUEPRINTS: CommonBlueprintEntry[] = [
51
52
  id: "c3",
52
53
  type: "score_icp_mcp",
53
54
  name: "ICP Score",
54
- config: { rubric: enrichmentRubric },
55
+ config: {
56
+ rubric: enrichmentRubric,
57
+ runCondition: "{{Enrich Prospect.id}} !== ''",
58
+ },
55
59
  inputMapping: { enrichedProspectId: "c2" },
56
60
  },
57
61
  ],
@@ -78,7 +82,10 @@ export const COMMON_BLUEPRINTS: CommonBlueprintEntry[] = [
78
82
  id: "c3",
79
83
  type: "score_icp_mcp",
80
84
  name: "ICP Score",
81
- config: { rubric: enrichmentRubric },
85
+ config: {
86
+ rubric: enrichmentRubric,
87
+ runCondition: "{{Enrich Prospect.id}} !== ''",
88
+ },
82
89
  inputMapping: { enrichedProspectId: "c2" },
83
90
  },
84
91
  {
@@ -131,7 +138,10 @@ export const COMMON_BLUEPRINTS: CommonBlueprintEntry[] = [
131
138
  id: "c4",
132
139
  type: "score_icp_mcp",
133
140
  name: "ICP Score",
134
- config: { rubric: enrichmentRubric },
141
+ config: {
142
+ rubric: enrichmentRubric,
143
+ runCondition: "{{Enrich Prospect.id}} !== ''",
144
+ },
135
145
  inputMapping: { enrichedProspectId: "c3" },
136
146
  },
137
147
  {
@@ -139,13 +149,15 @@ export const COMMON_BLUEPRINTS: CommonBlueprintEntry[] = [
139
149
  type: "generate_message",
140
150
  name: "Generate Message",
141
151
  config: {
142
- runCondition: "{{ICP Score.score}} >= 70",
152
+ runCondition:
153
+ "{{Enrich Prospect.id}} !== '' && {{ICP Score.score}} >= 70",
143
154
  },
144
155
  inputMapping: {
145
156
  campaignOfferId: "c2",
146
157
  enrichedProspectId: "c3",
147
158
  },
148
- runCondition: "{{ICP Score.score}} >= 70",
159
+ runCondition:
160
+ "{{Enrich Prospect.id}} !== '' && {{ICP Score.score}} >= 70",
149
161
  },
150
162
  ],
151
163
  edges: [
@@ -196,4 +208,41 @@ export const COMMON_BLUEPRINTS: CommonBlueprintEntry[] = [
196
208
  ],
197
209
  },
198
210
  },
211
+ {
212
+ slug: "rate-limited-http-and-webhook",
213
+ description:
214
+ "Receive webhook payloads and call a rate-limited HTTP enrichment API.",
215
+ blueprint: {
216
+ columns: [
217
+ { id: "c1", type: "text", name: "Company Domain", config: {} },
218
+ {
219
+ id: "c2",
220
+ type: "inbound_webhook",
221
+ name: "Inbound Payload",
222
+ config: {
223
+ rateLimit: { maxRequests: 100, windowSeconds: 60 },
224
+ },
225
+ },
226
+ {
227
+ id: "c3",
228
+ type: "http_request",
229
+ name: "Fetch Account Context",
230
+ config: {
231
+ inputMapping: {
232
+ method: { value: "POST" },
233
+ endpoint: { value: "https://example.test/account-context" },
234
+ body: { value: '{"domain":"{{Company Domain}}"}' },
235
+ },
236
+ rateLimit: {
237
+ mode: "window",
238
+ maxRequests: 10,
239
+ windowSeconds: 60,
240
+ },
241
+ },
242
+ inputMapping: { domain: "c1" },
243
+ },
244
+ ],
245
+ edges: [{ from: "c1", to: "c3", via: "domain" }],
246
+ },
247
+ },
199
248
  ];
@@ -42,3 +42,12 @@ Use `ai_column` only when the user needs a bespoke prompt that is not covered by
42
42
  fixed enrichment, scoring, or LinkedIn action columns.
43
43
 
44
44
  See `COMMON_BLUEPRINTS[3].blueprint` in the fixture.
45
+
46
+ ## 5. Rate-Limited HTTP + Webhook (`rate-limited-http-and-webhook`)
47
+
48
+ Use `inbound_webhook.config.rateLimit` for public webhook intake and
49
+ `http_request.config.rateLimit` for outbound HTTP calls. HTTP supports rolling
50
+ window limits with `mode: "window"`, `maxRequests`, and `windowSeconds`, or
51
+ concurrency limits with `mode: "concurrency"` and `maxConcurrent`.
52
+
53
+ See `COMMON_BLUEPRINTS[4].blueprint` in the fixture.
@@ -179,17 +179,11 @@ person/company this campaign is for, then I’ll turn that into a campaign brief
179
179
  before we move into lead sourcing.
180
180
  ```
181
181
 
182
- Exception: if `bootstrap_create_campaign.modelQuality.status === "warn"` and
183
- `bootstrap_create_campaign.modelQuality.metadataStale !== true`, the first
184
- visible campaign message must be the model-quality warning from
185
- `modelQuality.message`. Ask the user to switch to the configured minimum model
186
- or explicitly continue anyway before identity setup, research, lead filtering,
187
- message generation, or launch review. If `metadataStale === true`, continue
188
- normally and do not ask the user to switch.
189
-
190
- If `bootstrap_create_campaign.modelQuality.metadataStale === true`, continue
191
- normally. Do not ask the user to switch models; this is an accepted Codex host
192
- metadata mismatch.
182
+ Exception: if `bootstrap_create_campaign.modelQuality.status === "warn"`, the
183
+ first visible campaign message must be the model-quality warning from
184
+ `modelQuality.message`. Only trust that warning when bootstrap received active
185
+ turn/runtime metadata or an explicit user-confirmed model. Do not warn from
186
+ config defaults, stale host labels, or inferred model names.
193
187
 
194
188
  If a linked/local skill file is stale or missing, silently use the installed
195
189
  `sellable@sellable` plugin copy. Do not tell the user about the stale link,
@@ -207,6 +201,55 @@ clear business decisions, tradeoffs, and approval gates. Use product language:
207
201
  - "I can create a draft shell for you to watch with approval gates before
208
202
  sourcing", not mutation jargon
209
203
 
204
+ ## Active Model Metadata
205
+
206
+ Before calling `bootstrap_create_campaign`, collect the active host model data
207
+ only from a source that can describe this turn:
208
+
209
+ - Codex: if `mcp__node_repl__js` is available, inspect only
210
+ `nodeRepl.requestMeta["x-codex-turn-metadata"].model` and
211
+ `nodeRepl.requestMeta["x-codex-turn-metadata"].reasoning_effort`. Pass those
212
+ values as `model` and `reasoningEffort`, with
213
+ `modelMetadataSource: "codex_turn_metadata"`.
214
+ - Codex fallback/cross-check: if active turn metadata is unavailable, you may
215
+ inspect `~/.codex/config.toml` for `model` and `model_reasoning_effort`, but
216
+ pass `modelMetadataSource: "codex_config_fallback"` and do not treat it as a
217
+ reason to ask the user to switch models. Active turn metadata wins if it
218
+ differs from config.
219
+ - Claude Code / Opus: use active Claude runtime metadata only if the current
220
+ host exposes model and effort for this same session, with
221
+ `modelMetadataSource: "claude_runtime_metadata"`. Current Claude Code MCP
222
+ tool calls may not include model or effort metadata; when they do not, omit
223
+ `model`, `reasoningEffort`, and `modelMetadataSource`.
224
+ - Claude Code session-context self-report: if your current Claude session
225
+ context explicitly states both the exact model ID and effort/thinking level,
226
+ report it internally in this shape and pass it to bootstrap with
227
+ `modelMetadataSource: "claude_session_context"`:
228
+
229
+ ```text
230
+ - Model (name): ...
231
+ - Model (ID): ...
232
+ - Reasoning effort: ...
233
+ - Source: active Claude Code session context
234
+ ```
235
+
236
+ Use this only when the values are explicitly present in the current session
237
+ context. Do not infer an ID from the friendly name, do not infer effort from
238
+ `alwaysThinkingEnabled`, and do not show this self-report to the user during
239
+ normal campaign setup.
240
+ - Do not run a nested `claude -p`, inspect `~/.claude/settings.json`, or read
241
+ Claude CLI defaults as proof of the user's active Claude Code session. Those
242
+ checks can validate a new child session or saved defaults, but not this
243
+ session's actual model and effort.
244
+ - If the user explicitly provides active Claude `/status` or `/model` output
245
+ that includes both model and effort, pass it with
246
+ `modelMetadataSource: "user_confirmed"`. If it is missing either model or
247
+ effort, treat the metadata as unknown and continue.
248
+
249
+ Never invent the model or reasoning effort. Never pass config defaults as active
250
+ metadata. If bootstrap returns `modelQuality.status === "unknown"`, continue
251
+ without asking the user to switch models.
252
+
210
253
  Approval and safety copy should be tasteful. State what the current approval
211
254
  covers once, in one short sentence, then move on. Do not append repeated
212
255
  "nothing starts / no leads import / no sending" disclaimers to routine progress
@@ -775,10 +818,9 @@ messages, and wait for final launch approval.
775
818
  What's your LinkedIn profile URL or handle?
776
819
  ```
777
820
 
778
- Codex only: do not silently ask intake or approval questions as plain chat when
779
- `request_user_input` is unavailable in an interactive Codex session. Claude Code
780
- uses `AskUserQuestion`; do not apply this Codex setup blocker in Claude Code.
781
- In Codex, stop and tell the user:
821
+ Do not silently ask Codex intake or approval questions as plain chat when
822
+ `request_user_input` is unavailable in an interactive session. Stop and tell
823
+ the user:
782
824
 
783
825
  ```text
784
826
  I need Codex’s quick question panel to collect campaign inputs and approvals cleanly.
@@ -817,18 +859,17 @@ there.
817
859
  ## Bootstrap
818
860
 
819
861
  MCP tool access is required. First call `mcp__sellable__get_auth_status({})`
820
- directly. If that tool is unavailable, stop and say this is a Sellable
821
- install/reload problem for the current host, not a campaign problem. Tell the
822
- user to run `curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh` so
823
- the packaged MCP server and the current host integration are installed. If they
824
- want an agent-readable checklist, tell them:
862
+ directly. If that tool is unavailable, stop and say this is a Codex
863
+ install/reload problem, not a campaign problem. Tell the user to
864
+ run `curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh` so the
865
+ packaged MCP server, Codex Desktop plugin, and Sellable skill bundle are
866
+ installed. If they want an agent-readable checklist, tell them:
825
867
  `Install Sellable CLI and skills using https://app.sellable.dev/agent-install.txt`.
826
868
  For CLI verification, tell them to run
827
869
  `sellable --verify-only --host all --json --artifact "$HOME/.local/sellable/app-sellable-dev/installer/.last-verify.json"`.
828
- After that, they must fully quit and reopen the current host app before starting
829
- a new thread. In Codex, say Codex Desktop. In Claude Code, say Claude Code. Do
830
- not use `scripts/mcp/sellable-tool-call.mjs`, `npm run`, `node`, or any local
831
- harness as a fallback for this interactive skill.
870
+ After that, they must fully quit and reopen Codex Desktop before starting a new
871
+ thread. Do not use `scripts/mcp/sellable-tool-call.mjs`, `npm run`, `node`, or
872
+ any local harness as a fallback for this interactive skill.
832
873
  Do not mention prompt loading, local skill files, missing linked versions,
833
874
  plugin cache paths, MCP namespaces, or runbooks in customer-facing progress
834
875
  updates.
@@ -940,15 +981,19 @@ updates.
940
981
  - Do not call `mcp__sellable__get_campaigns`.
941
982
  - Do not call `mcp__sellable__get_campaign` to hunt for IDs.
942
983
  - Do not call `mcp__sellable__create_campaign({ campaignId: ... })` unless the user supplied that id.
943
- 6. Call `mcp__sellable__bootstrap_create_campaign({ flowVersion: "v2", campaignId?, host, model?, reasoningEffort? })`.
944
- Pass the explicit current host label: `host: "Codex"` from Codex and
945
- `host: "Claude Code"` from Claude Code. Also pass the current model and
946
- reasoning when the host exposes them.
984
+ 6. Call `mcp__sellable__bootstrap_create_campaign({ flowVersion: "v2", campaignId?, host?, model?, reasoningEffort?, modelMetadataSource? })`.
985
+ Pass model metadata only when collected by the Active Model Metadata rules
986
+ above. For Codex active turn metadata, pass
987
+ `modelMetadataSource: "codex_turn_metadata"`. For explicit Claude session
988
+ context, pass `modelMetadataSource: "claude_session_context"`. For explicit
989
+ user-confirmed Claude `/status` or `/model` output, pass
990
+ `modelMetadataSource: "user_confirmed"` only when it includes both model and
991
+ effort.
947
992
  7. If `safeToProceed !== true`, stop and show `blockingErrors` + `nextStep`.
948
- 8. If `modelQuality.status === "warn"` and `modelQuality.metadataStale !== true`,
949
- show `modelQuality.message` before any setup/research and wait for the user
950
- to switch or explicitly continue. If `metadataStale === true`, continue
951
- normally and do not tell the user to switch.
993
+ 8. If `modelQuality.status === "warn"`, show `modelQuality.message` before any
994
+ setup/research and wait for the user to switch or explicitly continue. If
995
+ `modelQuality.status === "unknown"`, continue; do not tell the user to
996
+ switch models.
952
997
 
953
998
  ## Execute Workflow
954
999
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": 1,
2
+ "version": 2,
3
3
  "hosts": {
4
4
  "claude": {
5
5
  "label": "Claude Code",
@@ -21,9 +21,8 @@
21
21
  }
22
22
  },
23
23
  "warningCopy": {
24
- "ok": "Campaign model settings meet the configured minimum: {minimumSummary}.",
25
- "staleCodexMetadata": "Codex host metadata appears stale: it can report GPT-5/default or GPT-5/high even when the UI is set to GPT 5.5 Extra High. Treating this as acceptable; continue without asking the user to switch.",
26
- "unknownSettings": "Model settings were not provided by the host. Best campaigns need at least {minimumSummary}. Confirm the user is on one of those settings, or ask them to switch before continuing.",
27
- "belowMinimum": "Best campaigns need at least {minimumSummary}. Current settings look below that: {currentSettings}. Please switch before continuing, or explicitly say to continue anyway."
24
+ "ok": "Active host model metadata meets the configured campaign floor: {currentSettings}.",
25
+ "skipped": "Active host model metadata was not available from a trusted runtime source. Continue without asking the user to switch models.",
26
+ "belowMinimum": "Active host metadata reports {currentSettings}, which is below the configured campaign floor: {minimumSummary}. Ask the user to switch before continuing, or explicitly say to continue anyway."
28
27
  }
29
28
  }
@@ -296,7 +296,12 @@ atomic-mint step:
296
296
 
297
297
  0. **verify workspace/sender invariant** (see §Preconditions) — if the
298
298
  invariant fails, abort BEFORE `bootstrap_create_campaign`
299
- 1. `bootstrap_create_campaign({ flowVersion: "v2", host?, model?, reasoningEffort? })`
299
+ 1. `bootstrap_create_campaign({ flowVersion: "v2", host?, model?, reasoningEffort?, modelMetadataSource? })`
300
+ Pass model metadata only when it comes from active turn/runtime metadata or
301
+ explicit user confirmation that includes both model and effort. Use
302
+ `modelMetadataSource: "claude_session_context"` only when the current Claude
303
+ session context explicitly states both values. Never use inferred config
304
+ defaults, nested Claude sessions, or saved settings files.
300
305
  2. If `campaign-shell.json` exists, call `create_campaign({ campaignId })` to
301
306
  resume/recover `{ campaignId, watchUrl }`, then
302
307
  `update_campaign({ campaignId, campaignBrief, leadSourceType,
@@ -0,0 +1,9 @@
1
+ {
2
+ "parallelMode": "wide",
3
+ "agentCount": 6,
4
+ "maxToolCallsPerAgent": 2,
5
+ "senderMaxAgents": 2,
6
+ "senderMaxToolCallsPerAgent": 3,
7
+ "progressMode": true,
8
+ "debugMode": true
9
+ }