@sellable/mcp 0.1.315 → 0.1.318

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/README.md CHANGED
@@ -302,11 +302,32 @@ Use the same MCP runtime preflight in both hosts:
302
302
 
303
303
  Canonical shortcut:
304
304
 
305
- - `bootstrap_create_campaign({ flowVersion: "v2", campaignId?, host?, model?, reasoningEffort? })`
305
+ - `bootstrap_create_campaign({ flowVersion: "v2", campaignId?, host?, model?, reasoningEffort?, modelMetadataSource? })`
306
+
307
+ Pass model/reasoning metadata only when it comes from active turn/runtime
308
+ metadata or explicit user confirmation. For Codex, use
309
+ `nodeRepl.requestMeta["x-codex-turn-metadata"]` and pass
310
+ `modelMetadataSource: "codex_turn_metadata"`. Config files such as
311
+ `~/.codex/config.toml` are fallback/cross-check evidence only and must not
312
+ trigger model-switch warnings.
313
+
314
+ For Claude Code, pass model metadata only when the current host exposes active
315
+ runtime model and effort for the same session, or when the user explicitly
316
+ confirms current `/status` or `/model` output that includes both model and effort.
317
+ If the current Claude session context explicitly states both values, use
318
+ `modelMetadataSource: "claude_session_context"`. Use
319
+ `modelMetadataSource: "user_confirmed"` for explicit user confirmation. Do not
320
+ use a nested `claude -p` run, `~/.claude/settings.json`, or CLI defaults as
321
+ active-session evidence.
322
+
323
+ Claude session-context self-report shape:
306
324
 
307
- Pass host/model/reasoning metadata when the host exposes it. The bootstrap
308
- model-quality check uses packaged config and warns before campaign setup when a
309
- known model is below the configured minimum.
325
+ ```text
326
+ - Model (name): ...
327
+ - Model (ID): ...
328
+ - Reasoning effort: ...
329
+ - Source: active Claude Code session context
330
+ ```
310
331
 
311
332
  If bootstrap returns blocking errors, fail fast and do not continue into provider
312
333
  search/import tools.
@@ -11,6 +11,7 @@ type BootstrapCreateCampaignInput = {
11
11
  host?: string;
12
12
  model?: string;
13
13
  reasoningEffort?: string;
14
+ modelMetadataSource?: string;
14
15
  };
15
16
  type BootstrapCheck = {
16
17
  key: "auth" | "update" | "framework" | "model_quality" | "resume_context" | "subskill_catalog" | "create_campaign_subskill";
@@ -87,6 +88,10 @@ export declare const bootstrapToolDefinitions: {
87
88
  type: string;
88
89
  description: string;
89
90
  };
91
+ modelMetadataSource: {
92
+ type: string;
93
+ description: string;
94
+ };
90
95
  };
91
96
  additionalProperties: boolean;
92
97
  };
@@ -18,7 +18,7 @@ function toGuidance(check, message) {
18
18
  return "Fix framework files and rerun bootstrap_create_campaign.";
19
19
  }
20
20
  if (check === "model_quality") {
21
- return `Use at least ${getCampaignModelMinimumSummary()} for campaign-critical generation.`;
21
+ return `Use trusted active-turn metadata before enforcing the campaign model floor: ${getCampaignModelMinimumSummary()}.`;
22
22
  }
23
23
  if (check === "resume_context") {
24
24
  return "Confirm the campaignId exists and is accessible, then retry bootstrap_create_campaign.";
@@ -98,15 +98,19 @@ export const bootstrapToolDefinitions = [
98
98
  },
99
99
  host: {
100
100
  type: "string",
101
- description: "Optional host label for model-quality preflight, for example Claude Code or Codex.",
101
+ description: "Optional host label for active model-quality preflight, for example Claude Code or Codex.",
102
102
  },
103
103
  model: {
104
104
  type: "string",
105
- description: `Optional current host model name. Minimum campaign quality config: ${getCampaignModelMinimumSummary()}.`,
105
+ description: "Optional active host model name from runtime metadata. Do not pass config defaults.",
106
106
  },
107
107
  reasoningEffort: {
108
108
  type: "string",
109
- description: `Optional current reasoning setting. Minimum campaign quality config: ${getCampaignModelMinimumSummary()}.`,
109
+ description: "Optional active reasoning setting from runtime metadata. Do not pass config defaults.",
110
+ },
111
+ modelMetadataSource: {
112
+ type: "string",
113
+ description: "Where active model metadata came from, for example codex_turn_metadata, claude_runtime_metadata, claude_session_context, active_turn_metadata, or user_confirmed.",
110
114
  },
111
115
  },
112
116
  additionalProperties: false,
@@ -129,6 +133,7 @@ export async function bootstrapCreateCampaign(input = {}) {
129
133
  host: input.host,
130
134
  model: input.model,
131
135
  reasoningEffort: input.reasoningEffort,
136
+ metadataSource: input.modelMetadataSource,
132
137
  });
133
138
  const runAuthCheck = async () => {
134
139
  let status = await getAuthStatus();
@@ -188,7 +193,7 @@ export async function bootstrapCreateCampaign(input = {}) {
188
193
  }
189
194
  requiredChecks.push({
190
195
  key: "model_quality",
191
- ok: modelQuality.status === "ok",
196
+ ok: modelQuality.status !== "warn",
192
197
  blocking: false,
193
198
  detail: modelQuality.message,
194
199
  });
@@ -3,6 +3,7 @@ export type CampaignModelQualityInput = {
3
3
  host?: string | null;
4
4
  model?: string | null;
5
5
  reasoningEffort?: string | null;
6
+ metadataSource?: string | null;
6
7
  };
7
8
  export type CampaignModelQualityStatus = "ok" | "warn" | "unknown";
8
9
  export type CampaignModelQualityResult = {
@@ -10,6 +11,7 @@ export type CampaignModelQualityResult = {
10
11
  host: CampaignModelHost;
11
12
  model: string | null;
12
13
  reasoningEffort: string | null;
14
+ metadataSource: string | null;
13
15
  recommendedModel: string;
14
16
  recommendedReasoningEffort: string;
15
17
  minimumSummary: string;
@@ -34,8 +36,7 @@ export type CampaignModelQualityConfig = {
34
36
  };
35
37
  warningCopy: {
36
38
  ok: string;
37
- staleCodexMetadata: string;
38
- unknownSettings: string;
39
+ skipped: string;
39
40
  belowMinimum: string;
40
41
  };
41
42
  };
@@ -1,8 +1,5 @@
1
- import { existsSync, readFileSync } from "fs";
2
- import { join } from "path";
3
- import { resolveSkillsDir } from "../skills.js";
4
1
  const DEFAULT_MODEL_QUALITY_CONFIG = {
5
- version: 1,
2
+ version: 2,
6
3
  hosts: {
7
4
  claude: {
8
5
  label: "Claude Code",
@@ -36,20 +33,24 @@ const DEFAULT_MODEL_QUALITY_CONFIG = {
36
33
  },
37
34
  },
38
35
  warningCopy: {
39
- ok: "Campaign model settings meet the configured minimum: {minimumSummary}.",
40
- 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.",
41
- 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.",
42
- belowMinimum: "Best campaigns need at least {minimumSummary}. Current settings look below that: {currentSettings}. Please switch before continuing, or explicitly say to continue anyway.",
36
+ ok: "Active host model metadata meets the configured campaign floor: {currentSettings}.",
37
+ skipped: "Active host model metadata was not available from a trusted runtime source. Continue without asking the user to switch models.",
38
+ 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.",
43
39
  },
44
40
  };
45
- let cachedConfig = null;
41
+ const TRUSTED_METADATA_SOURCE_KEYWORDS = [
42
+ "codex_turn_metadata",
43
+ "claude_runtime_metadata",
44
+ "claude_session_context",
45
+ "active_turn_metadata",
46
+ "user_confirmed",
47
+ ];
46
48
  const normalize = (value) => String(value ?? "")
47
49
  .trim()
48
50
  .toLowerCase();
49
51
  const normalizeHost = (host) => {
50
52
  const normalized = normalize(host);
51
53
  if (normalized.includes("claude") ||
52
- normalized.includes("clod") ||
53
54
  normalized.includes("opus") ||
54
55
  normalized.includes("sonnet") ||
55
56
  normalized.includes("haiku")) {
@@ -62,12 +63,6 @@ const normalizeHost = (host) => {
62
63
  }
63
64
  return "unknown";
64
65
  };
65
- const normalizeHostFromInput = (host, model) => {
66
- const explicitHost = normalizeHost(host);
67
- if (explicitHost !== "unknown")
68
- return explicitHost;
69
- return normalizeHost(model);
70
- };
71
66
  const normalizeReasoning = (reasoning) => normalize(reasoning).replace(/[_\s-]+/g, "");
72
67
  const compareVersion = (candidate, minimum) => {
73
68
  const candidateParts = candidate.split(".").map((part) => Number(part));
@@ -83,50 +78,12 @@ const compareVersion = (candidate, minimum) => {
83
78
  }
84
79
  return 0;
85
80
  };
86
- const extractModelVersions = (model) => Array.from(normalize(model).matchAll(/\b(\d+(?:\.\d+){0,2})\b/g)).map((match) => match[1]);
87
- function modelContainsOtherHostFamily(model, currentHostConfig, config) {
88
- const normalizedModel = normalize(model).replace(/[_-]+/g, " ");
89
- if (!normalizedModel)
90
- return false;
91
- return Object.values(config.hosts)
92
- .filter((hostConfig) => hostConfig !== currentHostConfig)
93
- .some((hostConfig) => hostConfig.familyKeywords.some((keyword) => normalizedModel.includes(normalize(keyword))));
94
- }
95
- function mergeConfig(rawConfig) {
96
- return {
97
- ...DEFAULT_MODEL_QUALITY_CONFIG,
98
- ...rawConfig,
99
- hosts: {
100
- claude: {
101
- ...DEFAULT_MODEL_QUALITY_CONFIG.hosts.claude,
102
- ...(rawConfig.hosts?.claude || {}),
103
- },
104
- codex: {
105
- ...DEFAULT_MODEL_QUALITY_CONFIG.hosts.codex,
106
- ...(rawConfig.hosts?.codex || {}),
107
- },
108
- },
109
- warningCopy: {
110
- ...DEFAULT_MODEL_QUALITY_CONFIG.warningCopy,
111
- ...(rawConfig.warningCopy || {}),
112
- },
113
- };
114
- }
81
+ const extractModelVersions = (model) => {
82
+ const normalized = normalize(model).replace(/(?<=\d)[_-](?=\d)/g, ".");
83
+ return Array.from(normalized.matchAll(/\b(\d+(?:\.\d+){0,2})\b/g)).map((match) => match[1]);
84
+ };
115
85
  export function getCampaignModelQualityConfig() {
116
- if (cachedConfig)
117
- return cachedConfig;
118
- const configPath = join(resolveSkillsDir(), "create-campaign", "core", "model-quality.json");
119
- if (!existsSync(configPath)) {
120
- cachedConfig = DEFAULT_MODEL_QUALITY_CONFIG;
121
- return cachedConfig;
122
- }
123
- try {
124
- cachedConfig = mergeConfig(JSON.parse(readFileSync(configPath, "utf-8")));
125
- }
126
- catch {
127
- cachedConfig = DEFAULT_MODEL_QUALITY_CONFIG;
128
- }
129
- return cachedConfig;
86
+ return DEFAULT_MODEL_QUALITY_CONFIG;
130
87
  }
131
88
  export function getCampaignModelMinimumSummary(config = getCampaignModelQualityConfig()) {
132
89
  return `${config.hosts.claude.minimumModel} with ${config.hosts.claude.minimumReasoningEffort} reasoning or ${config.hosts.codex.minimumModel} with ${config.hosts.codex.minimumReasoningEffort} reasoning`;
@@ -148,12 +105,12 @@ function modelMeetsMinimum(model, hostConfig, options = {}) {
148
105
  hostConfig.familyKeywords.every((keyword) => normalizedModel.includes(normalize(keyword)));
149
106
  if (!familyMatches)
150
107
  return false;
151
- const versions = extractModelVersions(normalizedModel);
108
+ const versions = extractModelVersions(model);
152
109
  if (!hostConfig.minimumVersion)
153
110
  return true;
154
111
  return versions.some((version) => compareVersion(version, hostConfig.minimumVersion) >= 0);
155
112
  }
156
- function findAcceptedHostConfig(host, model, config) {
113
+ function findHostConfig(host, model, config) {
157
114
  const candidates = host === "unknown"
158
115
  ? [
159
116
  ["claude", config.hosts.claude],
@@ -161,104 +118,66 @@ function findAcceptedHostConfig(host, model, config) {
161
118
  ]
162
119
  : [[host, config.hosts[host]]];
163
120
  return candidates.find(([, hostConfig]) => modelMeetsMinimum(model, hostConfig, {
164
- familyKnownFromHost: host !== "unknown" &&
165
- !modelContainsOtherHostFamily(model, hostConfig, config),
121
+ familyKnownFromHost: host !== "unknown",
166
122
  }));
167
123
  }
168
- function looksLikeCodexStaleMetadata(host, model, reasoningEffort, config) {
169
- if (host !== "codex")
170
- return false;
171
- if (!model || !reasoningEffort)
172
- return false;
173
- const normalizedModel = normalize(model).replace(/[_-]+/g, " ");
174
- const normalizedReasoning = normalizeReasoning(reasoningEffort);
175
- if (!normalizedModel.includes("gpt"))
176
- return false;
177
- const versions = extractModelVersions(normalizedModel);
178
- const looksLikeBaseGpt5 = versions.some((version) => version === "5" || version === "5.0");
179
- if (!looksLikeBaseGpt5)
180
- return false;
181
- return (["default", "auto", "standard"].includes(normalizedReasoning) ||
182
- acceptsReasoning(reasoningEffort, config.hosts.codex));
124
+ function isTrustedMetadataSource(source) {
125
+ const normalized = normalize(source).replace(/[\s-]+/g, "_");
126
+ return TRUSTED_METADATA_SOURCE_KEYWORDS.some((keyword) => normalized.includes(keyword));
183
127
  }
184
128
  export function evaluateCampaignModelQuality(input = {}) {
185
129
  const config = getCampaignModelQualityConfig();
186
- const host = normalizeHostFromInput(input.host, input.model);
130
+ const host = normalizeHost([input.host, input.model].filter(Boolean).join(" "));
187
131
  const model = input.model?.trim() || null;
188
132
  const reasoningEffort = input.reasoningEffort?.trim() || null;
133
+ const metadataSource = input.metadataSource?.trim() || null;
189
134
  const recommendationHost = host === "claude" ? "claude" : "codex";
190
135
  const recommendedHostConfig = config.hosts[recommendationHost];
191
136
  const minimumSummary = getCampaignModelMinimumSummary(config);
192
- const recommendedModel = recommendedHostConfig.minimumModel;
193
- const recommendedReasoningEffort = recommendedHostConfig.recommendedReasoningEffort;
194
- if (!model && !reasoningEffort) {
137
+ const currentSettings = [
138
+ model ? `model "${model}"` : "unknown model",
139
+ reasoningEffort ? `reasoning "${reasoningEffort}"` : "unknown reasoning",
140
+ metadataSource ? `source "${metadataSource}"` : "unknown source",
141
+ ].join(", ");
142
+ const base = {
143
+ host,
144
+ model,
145
+ reasoningEffort,
146
+ metadataSource,
147
+ recommendedModel: recommendedHostConfig.minimumModel,
148
+ recommendedReasoningEffort: recommendedHostConfig.recommendedReasoningEffort,
149
+ minimumSummary,
150
+ metadataStale: false,
151
+ };
152
+ if (!isTrustedMetadataSource(metadataSource) || !model || !reasoningEffort) {
195
153
  return {
154
+ ...base,
196
155
  status: "unknown",
197
- host,
198
- model,
199
- reasoningEffort,
200
- recommendedModel,
201
- recommendedReasoningEffort,
202
- minimumSummary,
203
- confirmationRequired: true,
204
- metadataStale: false,
205
- message: formatCopy(config.warningCopy.unknownSettings, {
206
- minimumSummary,
207
- currentSettings: "unknown",
208
- }),
156
+ confirmationRequired: false,
157
+ message: config.warningCopy.skipped,
209
158
  };
210
159
  }
211
- const acceptedHostConfig = findAcceptedHostConfig(host, model, config);
160
+ const acceptedHostConfig = findHostConfig(host, model, config);
212
161
  const ok = Boolean(acceptedHostConfig && acceptsReasoning(reasoningEffort, acceptedHostConfig[1]));
213
162
  if (ok) {
214
163
  return {
164
+ ...base,
215
165
  status: "ok",
216
166
  host: acceptedHostConfig?.[0] ?? host,
217
- model,
218
- reasoningEffort,
219
- recommendedModel: acceptedHostConfig?.[1].minimumModel ?? recommendedModel,
167
+ recommendedModel: acceptedHostConfig?.[1].minimumModel ?? base.recommendedModel,
220
168
  recommendedReasoningEffort: acceptedHostConfig?.[1].recommendedReasoningEffort ??
221
- recommendedReasoningEffort,
222
- minimumSummary,
169
+ base.recommendedReasoningEffort,
223
170
  confirmationRequired: false,
224
- metadataStale: false,
225
171
  message: formatCopy(config.warningCopy.ok, {
226
172
  minimumSummary,
227
- currentSettings: "current settings",
173
+ currentSettings,
228
174
  }),
229
175
  };
230
176
  }
231
- if (looksLikeCodexStaleMetadata(host, model, reasoningEffort, config)) {
232
- return {
233
- status: "ok",
234
- host,
235
- model,
236
- reasoningEffort,
237
- recommendedModel,
238
- recommendedReasoningEffort,
239
- minimumSummary,
240
- confirmationRequired: false,
241
- metadataStale: true,
242
- message: formatCopy(config.warningCopy.staleCodexMetadata, {
243
- minimumSummary,
244
- currentSettings: "stale Codex host metadata",
245
- }),
246
- };
247
- }
248
- const currentSettings = [
249
- model ? `model "${model}"` : "unknown model",
250
- reasoningEffort ? `reasoning "${reasoningEffort}"` : "unknown reasoning",
251
- ].join(", ");
252
177
  return {
178
+ ...base,
253
179
  status: "warn",
254
- host,
255
- model,
256
- reasoningEffort,
257
- recommendedModel,
258
- recommendedReasoningEffort,
259
- minimumSummary,
260
180
  confirmationRequired: true,
261
- metadataStale: false,
262
181
  message: formatCopy(config.warningCopy.belowMinimum, {
263
182
  minimumSummary,
264
183
  currentSettings,
@@ -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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.315",
3
+ "version": "0.1.318",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -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,