@sellable/mcp 0.1.27 → 0.1.29

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
@@ -33,7 +33,7 @@ command.
33
33
  For Claude Code and Codex CLI:
34
34
 
35
35
  ```bash
36
- npx -y @sellable/install --host all --token skt_live_your_token_here --workspace-id your_workspace_id
36
+ npx -y @sellable/install@latest --host all --token skt_live_your_token_here --workspace-id your_workspace_id
37
37
  ```
38
38
 
39
39
  Agent-readable install instructions are available at:
@@ -52,6 +52,24 @@ sh /tmp/sellable-install.sh
52
52
  Phase 112 v1 uses package stdio MCP through `@sellable/mcp`. Hosted HTTP
53
53
  MCP is a future/advanced mode until the hosted endpoint exists.
54
54
 
55
+ Package-mode installs launch `@sellable/mcp@latest`, so each fresh host MCP
56
+ start resolves the newest stable npm release. The MCP server also runs a cached
57
+ startup/auth update check and tells the agent to run
58
+ `npx -y @sellable/install@latest` when the installed runtime is behind npm.
59
+
60
+ ### Publishing `@sellable/mcp`
61
+
62
+ Publish from the repo root with:
63
+
64
+ ```bash
65
+ npm run mcp:publish
66
+ ```
67
+
68
+ The script loads `.env` and `prod.env`, builds `mcp/sellable`, verifies npm auth,
69
+ and publishes with a temporary npm config file. Prefer `NPM_TOKEN=<npm token>` in
70
+ `prod.env`; `NODE_AUTH_TOKEN`, `SELLABLE_NPM_TOKEN`, and `NPM_PUBLISH_TOKEN` are
71
+ also supported.
72
+
55
73
  ### 2. Generate API Token
56
74
 
57
75
  1. Go to https://app.sellable.dev/settings
@@ -63,7 +81,7 @@ MCP is a future/advanced mode until the hosted endpoint exists.
63
81
  Install MCP server:
64
82
 
65
83
  ```bash
66
- claude mcp add --transport stdio sellable -- npx -y @sellable/mcp
84
+ claude mcp add --transport stdio sellable -- npx -y @sellable/mcp@latest
67
85
  ```
68
86
 
69
87
  Create auth config at `~/.sellable/config.json`:
@@ -83,14 +101,14 @@ The token is provided when you generate it. Use `list_workspaces` +
83
101
  For customer/package installs, use the public installer:
84
102
 
85
103
  ```bash
86
- npx -y @sellable/install@0.1.26 --host codex --token skt_live_your_token_here --workspace-id your_workspace_id
104
+ npx -y @sellable/install@latest --host codex --token skt_live_your_token_here --workspace-id your_workspace_id
87
105
  ```
88
106
 
89
107
  If you already have `~/.sellable/config.json`, rerun/verify without rewriting
90
108
  auth:
91
109
 
92
110
  ```bash
93
- npx -y @sellable/install@0.1.26 --host codex
111
+ npx -y @sellable/install@latest --host codex
94
112
  sellable --verify-only --host codex
95
113
  ```
96
114
 
package/dist/index-dev.js CHANGED
File without changes
package/dist/index.js CHANGED
File without changes
package/dist/server.js CHANGED
@@ -29,6 +29,7 @@ import { attachRecommendedSequence, attachSequence, createWorkflowTable, sequenc
29
29
  import { listTables, tableToolDefinitions } from "./tools/tables.js";
30
30
  import { handleVerifyTableRow, verifyRowToolDefinitions, } from "./tools/verify-row.js";
31
31
  import { addTeammate, createWorkspace, getActiveWorkspace, listWorkspaces, setActiveWorkspace, workspaceToolDefinitions, } from "./tools/workspaces.js";
32
+ import { checkForUpdates, logUpdateNotice } from "./update-check.js";
32
33
  const server = new Server({
33
34
  name: "sellable-mcp",
34
35
  version: "1.0.0",
@@ -515,6 +516,11 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
515
516
  async function main() {
516
517
  const startTime = Date.now();
517
518
  console.error(`[sellable-mcp] pid=${process.pid} starting...`);
519
+ void checkForUpdates()
520
+ .then(logUpdateNotice)
521
+ .catch(() => {
522
+ // Update checks are advisory and must never block MCP startup.
523
+ });
518
524
  const transport = new StdioServerTransport();
519
525
  // Exit cleanly when parent disconnects stdin (Claude Code closed/reconnected)
520
526
  process.stdin.on("end", () => {
@@ -1,3 +1,4 @@
1
+ import { type SellableUpdateStatus } from "../update-check.js";
1
2
  export type AuthStatus = {
2
3
  ok: boolean;
3
4
  configPath: string;
@@ -10,6 +11,7 @@ export type AuthStatus = {
10
11
  checkedAt: string;
11
12
  /** Short message the LLM MUST show the user after auth succeeds. */
12
13
  _userNotice: string | null;
14
+ update: SellableUpdateStatus | null;
13
15
  error: null | {
14
16
  type: "config" | "auth" | "workspace" | "api";
15
17
  status?: number;
@@ -1,14 +1,28 @@
1
1
  import { getApi, SellableApiError } from "../api.js";
2
2
  import { getConfig, getConfigPath } from "../auth.js";
3
+ import { checkForUpdates } from "../update-check.js";
3
4
  function maskToken(token) {
4
5
  if (token.length <= 10)
5
6
  return `${token.slice(0, 4)}...`;
6
7
  return `${token.slice(0, 8)}...${token.slice(-2)}`;
7
8
  }
9
+ async function getUpdateStatus() {
10
+ try {
11
+ return await checkForUpdates();
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ function appendUpdateNotice(notice, update) {
18
+ if (!update?._userNotice)
19
+ return notice;
20
+ return notice ? `${notice}\n${update._userNotice}` : update._userNotice;
21
+ }
8
22
  export const authToolDefinitions = [
9
23
  {
10
24
  name: "get_auth_status",
11
- description: "Verify Sellable authentication early. Call this before any campaign work. Returns ok=false with clear guidance when the token is missing, revoked, or the active workspace is not set. IMPORTANT: When auth succeeds, ALWAYS tell the user the _userNotice message so they know which workspace is active.",
25
+ description: "Verify Sellable authentication early. Call this before any campaign work. Also performs a cached Sellable package update check. Returns ok=false with clear guidance when the token is missing, revoked, or the active workspace is not set. IMPORTANT: When auth succeeds, ALWAYS tell the user the _userNotice message so they know which workspace is active and whether an update is available.",
12
26
  inputSchema: {
13
27
  type: "object",
14
28
  properties: {},
@@ -20,6 +34,7 @@ export const authToolDefinitions = [
20
34
  export async function getAuthStatus() {
21
35
  const configPath = getConfigPath();
22
36
  const checkedAt = new Date().toISOString();
37
+ const update = await getUpdateStatus();
23
38
  const base = {
24
39
  ok: false,
25
40
  configPath,
@@ -29,7 +44,8 @@ export async function getAuthStatus() {
29
44
  activeWorkspaceName: null,
30
45
  tokenPrefix: null,
31
46
  workspacesCount: null,
32
- _userNotice: null,
47
+ _userNotice: appendUpdateNotice(null, update),
48
+ update,
33
49
  checkedAt,
34
50
  };
35
51
  try {
@@ -87,7 +103,8 @@ export async function getAuthStatus() {
87
103
  activeWorkspaceName: workspaceName,
88
104
  tokenPrefix: maskToken(config.token),
89
105
  workspacesCount: workspaces.length,
90
- _userNotice: notice,
106
+ _userNotice: appendUpdateNotice(notice, update),
107
+ update,
91
108
  checkedAt,
92
109
  error: null,
93
110
  };
@@ -9,7 +9,7 @@ type BootstrapCreateCampaignInput = {
9
9
  refreshContext?: boolean;
10
10
  };
11
11
  type BootstrapCheck = {
12
- key: "auth" | "framework" | "resume_context" | "subskill_catalog" | "create_campaign_subskill";
12
+ key: "auth" | "update" | "framework" | "resume_context" | "subskill_catalog" | "create_campaign_subskill";
13
13
  ok: boolean;
14
14
  blocking: boolean;
15
15
  detail: string;
@@ -10,6 +10,9 @@ function toGuidance(check, message) {
10
10
  if (check === "auth") {
11
11
  return "Run get_auth_status and fix auth/workspace setup before continuing.";
12
12
  }
13
+ if (check === "update") {
14
+ return "Run npx -y @sellable/install@latest, then restart Claude Code/Codex so Sellable reloads.";
15
+ }
13
16
  if (check === "framework") {
14
17
  return "Fix framework files and rerun bootstrap_create_campaign.";
15
18
  }
@@ -101,6 +104,22 @@ export async function bootstrapCreateCampaign(input = {}) {
101
104
  if (!ok) {
102
105
  pushBlockingError(blockingErrors, "auth", authCheck.status.error?.message || "Authentication check failed.");
103
106
  }
107
+ const update = authCheck.status.update;
108
+ if (update && !update.disabled) {
109
+ requiredChecks.push({
110
+ key: "update",
111
+ ok: !update.blocking,
112
+ blocking: update.blocking,
113
+ detail: update.updateAvailable
114
+ ? update._userNotice || "A newer Sellable MCP package is available."
115
+ : update.mcp.latestVersion
116
+ ? `Sellable MCP is current (${update.mcp.currentVersion ?? "unknown"}).`
117
+ : "Sellable MCP update check completed without latest-version data.",
118
+ });
119
+ if (update.blocking) {
120
+ pushBlockingError(blockingErrors, "update", update._userNotice || "Sellable MCP update required.");
121
+ }
122
+ }
104
123
  }
105
124
  catch (error) {
106
125
  const message = toErrorMessage(error);
@@ -2205,10 +2205,18 @@ export async function confirmLeadList(input) {
2205
2205
  ...(shouldSetCurrentStep ? { currentStep: effectiveCurrentStep } : {}),
2206
2206
  });
2207
2207
  const campaignTableId = importResult.workflowTableId ?? importResult.campaignTableId;
2208
- if (campaignTableId) {
2208
+ // Persist currentStep if the caller asked for it. Do NOT touch
2209
+ // selectedLeadListId here: the campaign table id is already saved to
2210
+ // CampaignOffer.workflowTableId by /api/v3/campaign-builder/import-leads,
2211
+ // and selectedLeadListId must remain pointing at the source lead list
2212
+ // (config.type === "lead_list") so the UI's Continue handler — which feeds
2213
+ // selectedLeadListId back into /api/v3/campaign-builder/import-leads as
2214
+ // sourceLeadListId — can find a valid lead list. Overwriting it with the
2215
+ // campaign table id makes that endpoint 404 with "Lead list not found"
2216
+ // and silently breaks the Leads → Filter Continue button.
2217
+ if (shouldSetCurrentStep) {
2209
2218
  await api.put(`/api/v2/campaign-offers/${campaignOfferId}`, {
2210
- selectedLeadListId: campaignTableId,
2211
- ...(shouldSetCurrentStep ? { currentStep: effectiveCurrentStep } : {}),
2219
+ currentStep: effectiveCurrentStep,
2212
2220
  });
2213
2221
  }
2214
2222
  return {
@@ -0,0 +1,25 @@
1
+ export type PackageUpdateStatus = {
2
+ packageName: string;
3
+ currentVersion: string | null;
4
+ latestVersion: string | null;
5
+ outdated: boolean;
6
+ error?: string;
7
+ };
8
+ export type SellableUpdateStatus = {
9
+ checkedAt: string;
10
+ cachePath: string;
11
+ fromCache: boolean;
12
+ disabled: boolean;
13
+ mcp: PackageUpdateStatus;
14
+ installer: PackageUpdateStatus;
15
+ updateAvailable: boolean;
16
+ blocking: boolean;
17
+ guidance: string | null;
18
+ _userNotice: string | null;
19
+ };
20
+ export declare function checkForUpdates(options?: {
21
+ force?: boolean;
22
+ timeoutMs?: number;
23
+ ttlMs?: number;
24
+ }): Promise<SellableUpdateStatus>;
25
+ export declare function logUpdateNotice(status: SellableUpdateStatus): void;
@@ -0,0 +1,210 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ const MCP_PACKAGE = "@sellable/mcp";
5
+ const INSTALL_PACKAGE = "@sellable/install";
6
+ const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
7
+ const DEFAULT_TIMEOUT_MS = 1500;
8
+ function cachePath() {
9
+ return path.join(os.homedir(), ".sellable", "update-check.json");
10
+ }
11
+ function readCurrentVersion() {
12
+ const entrypointDir = process.argv[1]
13
+ ? path.dirname(path.resolve(process.argv[1]))
14
+ : process.cwd();
15
+ const candidates = [
16
+ path.resolve(entrypointDir, "..", "package.json"),
17
+ path.resolve(process.cwd(), "mcp/sellable/package.json"),
18
+ ];
19
+ for (const pkgPath of candidates) {
20
+ try {
21
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
22
+ if (pkg.name === MCP_PACKAGE && typeof pkg.version === "string") {
23
+ return pkg.version;
24
+ }
25
+ }
26
+ catch {
27
+ // Keep looking; version detection is advisory only.
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+ function registryPackagePath(packageName) {
33
+ return encodeURIComponent(packageName);
34
+ }
35
+ async function fetchLatestVersion(packageName, timeoutMs) {
36
+ const controller = new AbortController();
37
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
38
+ try {
39
+ const response = await fetch(`https://registry.npmjs.org/${registryPackagePath(packageName)}/latest`, {
40
+ signal: controller.signal,
41
+ headers: {
42
+ Accept: "application/json",
43
+ "User-Agent": "sellable-mcp-update-check",
44
+ },
45
+ });
46
+ if (!response.ok) {
47
+ throw new Error(`npm registry returned ${response.status}`);
48
+ }
49
+ const body = (await response.json());
50
+ if (typeof body.version !== "string" || !body.version) {
51
+ throw new Error("npm registry response did not include a version");
52
+ }
53
+ return body.version;
54
+ }
55
+ finally {
56
+ clearTimeout(timer);
57
+ }
58
+ }
59
+ function parseVersion(version) {
60
+ if (!version)
61
+ return null;
62
+ const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
63
+ if (!match)
64
+ return null;
65
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
66
+ }
67
+ function isVersionOutdated(currentVersion, latestVersion) {
68
+ const current = parseVersion(currentVersion);
69
+ const latest = parseVersion(latestVersion);
70
+ if (!current || !latest)
71
+ return false;
72
+ for (let i = 0; i < 3; i += 1) {
73
+ if (current[i] < latest[i])
74
+ return true;
75
+ if (current[i] > latest[i])
76
+ return false;
77
+ }
78
+ return false;
79
+ }
80
+ function readCache(ttlMs) {
81
+ const file = cachePath();
82
+ try {
83
+ const raw = JSON.parse(fs.readFileSync(file, "utf8"));
84
+ if (!raw.checkedAtMs || Date.now() - raw.checkedAtMs > ttlMs)
85
+ return null;
86
+ return {
87
+ ...raw,
88
+ cachePath: file,
89
+ fromCache: true,
90
+ };
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ }
96
+ function writeCache(status) {
97
+ const file = cachePath();
98
+ try {
99
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
100
+ const { fromCache: _fromCache, ...cacheableStatus } = status;
101
+ const cache = {
102
+ ...cacheableStatus,
103
+ cachePath: file,
104
+ checkedAtMs: Date.now(),
105
+ };
106
+ fs.writeFileSync(file, `${JSON.stringify(cache, null, 2)}\n`, {
107
+ mode: 0o600,
108
+ });
109
+ }
110
+ catch {
111
+ // Update checks must never break MCP startup or tool execution.
112
+ }
113
+ }
114
+ function disabledStatus(currentVersion) {
115
+ return {
116
+ checkedAt: new Date().toISOString(),
117
+ cachePath: cachePath(),
118
+ fromCache: false,
119
+ disabled: true,
120
+ mcp: {
121
+ packageName: MCP_PACKAGE,
122
+ currentVersion,
123
+ latestVersion: null,
124
+ outdated: false,
125
+ },
126
+ installer: {
127
+ packageName: INSTALL_PACKAGE,
128
+ currentVersion: null,
129
+ latestVersion: null,
130
+ outdated: false,
131
+ },
132
+ updateAvailable: false,
133
+ blocking: false,
134
+ guidance: null,
135
+ _userNotice: null,
136
+ };
137
+ }
138
+ function buildNotice(mcp, installer) {
139
+ if (!mcp.outdated)
140
+ return null;
141
+ const versions = mcp.currentVersion && mcp.latestVersion
142
+ ? `${mcp.currentVersion} -> ${mcp.latestVersion}`
143
+ : "a newer version is available";
144
+ const installerVersion = installer.latestVersion
145
+ ? ` The latest installer is ${installer.latestVersion}.`
146
+ : "";
147
+ return `Sellable update available: ${versions}. Run: npx -y @sellable/install@latest.${installerVersion}`;
148
+ }
149
+ export async function checkForUpdates(options) {
150
+ const currentVersion = readCurrentVersion();
151
+ const ttlMs = options?.ttlMs ??
152
+ (Number(process.env.SELLABLE_UPDATE_CHECK_TTL_MS || "") || DEFAULT_TTL_MS);
153
+ const timeoutMs = options?.timeoutMs ??
154
+ (Number(process.env.SELLABLE_UPDATE_CHECK_TIMEOUT_MS || "") ||
155
+ DEFAULT_TIMEOUT_MS);
156
+ if (process.env.SELLABLE_UPDATE_CHECK_DISABLED === "1") {
157
+ return disabledStatus(currentVersion);
158
+ }
159
+ if (!options?.force) {
160
+ const cached = readCache(ttlMs);
161
+ if (cached)
162
+ return cached;
163
+ }
164
+ const [mcpResult, installerResult] = await Promise.allSettled([
165
+ fetchLatestVersion(MCP_PACKAGE, timeoutMs),
166
+ fetchLatestVersion(INSTALL_PACKAGE, timeoutMs),
167
+ ]);
168
+ const mcpLatest = mcpResult.status === "fulfilled" ? mcpResult.value : null;
169
+ const installerLatest = installerResult.status === "fulfilled" ? installerResult.value : null;
170
+ const mcp = {
171
+ packageName: MCP_PACKAGE,
172
+ currentVersion,
173
+ latestVersion: mcpLatest,
174
+ outdated: isVersionOutdated(currentVersion, mcpLatest),
175
+ ...(mcpResult.status === "rejected"
176
+ ? { error: mcpResult.reason?.message || String(mcpResult.reason) }
177
+ : {}),
178
+ };
179
+ const installer = {
180
+ packageName: INSTALL_PACKAGE,
181
+ currentVersion: null,
182
+ latestVersion: installerLatest,
183
+ outdated: false,
184
+ ...(installerResult.status === "rejected"
185
+ ? {
186
+ error: installerResult.reason?.message || String(installerResult.reason),
187
+ }
188
+ : {}),
189
+ };
190
+ const notice = buildNotice(mcp, installer);
191
+ const status = {
192
+ checkedAt: new Date().toISOString(),
193
+ cachePath: cachePath(),
194
+ fromCache: false,
195
+ disabled: false,
196
+ mcp,
197
+ installer,
198
+ updateAvailable: mcp.outdated,
199
+ blocking: false,
200
+ guidance: notice ? "Run npx -y @sellable/install@latest" : null,
201
+ _userNotice: notice,
202
+ };
203
+ writeCache(status);
204
+ return status;
205
+ }
206
+ export function logUpdateNotice(status) {
207
+ if (status._userNotice) {
208
+ console.error(`[sellable-mcp] ${status._userNotice}`);
209
+ }
210
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -118,6 +118,8 @@ Validated draft directory:
118
118
  loading, runbooks, local skill files, skill versions, or "Sellable token".
119
119
  "Quick question panel" is only acceptable when explaining a Codex/Claude setup
120
120
  blocker.
121
+ - Before campaign mint, do not call `check_rubric`; Phase 84 only writes and
122
+ validates draft artifacts.
121
123
  - Never narrate local draft housekeeping to the user. If you create directories,
122
124
  save drafts, write artifacts, or persist intermediate state, translate it into
123
125
  the campaign benefit: consistent brief, approved lead source, reviewed message,
@@ -394,6 +396,9 @@ should test for this campaign. Those can run in parallel and usually take
394
396
  - Before the user chooses `approve`, the mutating live-campaign tools are
395
397
  forbidden: `create_campaign`, `save_rubrics`, `import_leads`,
396
398
  `confirm_lead_list`, `update_campaign`, `queue_cells`, and `start_campaign`.
399
+ - During pre-approval validation, do not call `check_rubric`; use the
400
+ campaignless lead-filter artifacts and only use campaign-backed scoring after
401
+ the approval flow allows it.
397
402
  - Resume state is based on the presence and completeness of the chained artifacts,
398
403
  not on inline validation blocks inside `brief.md`.
399
404
  - Preserve the Phase 83 thesis. Do not rewrite product, ICP, offer, or message
@@ -868,34 +873,40 @@ Orchestration requirements:
868
873
  before category nouns like `platform` or `MCP`.
869
874
  - pass the Thomas revision filters before writing findings
870
875
  - immediately after `message-validation.md` is confirmed and reconciled with
871
- `lead-filter.md`, write `message-review.md` and render it inline starting
872
- with `Status: message-review` as the first visible line. Do not put a
873
- markdown heading, preface, or summary before that status line. This is a
874
- customer checkpoint, not an audit report: show the approved campaign message
875
- template first, then show one filled sample prospect version immediately after
876
- it so the user can judge whether the tokens are being used correctly. Then
877
- show rendered examples that prove the tokens fill well and can be omitted
878
- cleanly when row data is weak. Keep the detailed
879
- gold-standard/rule audit inside `message-validation.md`, not in the
880
- user-facing review. The review must use this exact label shape so the gate can
881
- be parsed: `Subject: ...`, `Tokenized template: ...`, `Sample prospect fill:
882
- ...`, `Rendered examples: ...`, `Good token fill: ...`, `Good omit: ...`,
883
- `Bad token fill: ...`, `Why bad: ...`, `Fallback if missing: ...`,
884
- `Token notes: ...`, `My take: ...`, `Suggested adjustment: ...`,
885
- `Question: approve-message or revise-messaging?`, `Recommendation:
886
- approve-message|revise-messaging`.
887
- - Never ask the message approval question until the full message review is
888
- visible in the chat. A summary like `Message review is ready` or `the draft
889
- avoids a generic pitch` is not enough. The user must see the actual subject,
890
- the actual tokenized message template, one filled sample prospect version, at
891
- least one good filled example, one good omit example, one bad fill example,
892
- why the bad fill is wrong, fallback rules for missing row data, token notes,
893
- your take, and the suggested adjustment before any `approve-message` /
894
- `revise-messaging` question appears. If you catch yourself wanting to offer
895
- `show me message` as a choice, stop: render the message review first, then ask
896
- only `approve-message` or `revise-messaging`.
897
- Include `Open artifacts:` links to `message-review.md` and
898
- `message-validation.md` before the approval question.
876
+ `lead-filter.md`, write `message-review.md` and render the slim chat version
877
+ inline starting with `Status: message-review` as the first visible line. Do
878
+ not put a markdown heading, preface, or summary before that status line.
879
+ This is a customer checkpoint optimized for fast approve / revise judgment.
880
+ - Chat-vs-file split (this is the rule the user pinned):
881
+ - `message-review.md` (the file) keeps the full detail: `Status:
882
+ message-review`, `Subject:`, `Tokenized template:`, `Sample prospect fill:` + `Token fill basis:`, `Rendered examples:`, `Good token fill:`,
883
+ `Good omit:`, `Bad token fill:`, `Why bad:`, `Fallback if missing:`,
884
+ `Token notes:`, `My take:`, `Suggested adjustment:`, `Question:
885
+ approve-message or revise-messaging?`, `Recommendation:
886
+ approve-message|revise-messaging`. The detailed gold-standard/rule audit
887
+ stays in `message-validation.md`. Both files are unchanged in scope and
888
+ are still required.
889
+ - The chat rendering shows ONLY: `Status: message-review`, `Subject:`,
890
+ `Tokenized template:`, `Sample prospect fill:` (with `Token fill basis:`),
891
+ `My take:`, `Suggested adjustment:`, `Question: approve-message or
892
+ revise-messaging?`, `Recommendation:`. Then `Open artifacts:` links to
893
+ `message-review.md` and `message-validation.md` so the user can read the
894
+ full token guidance, good/omit/bad examples, and fallbacks if they want
895
+ to before deciding.
896
+ - Do NOT inline `Rendered examples:`, `Good token fill:`, `Good omit:`,
897
+ `Bad token fill:`, `Why bad:`, `Fallback if missing:`, or `Token notes:`
898
+ in chat. Those exist in the file and are linked. Keeping them out of
899
+ chat is what makes the gate easy to approve or revise on.
900
+ - Never ask the message approval question until the slim chat review is fully
901
+ visible. A summary like `Message review is ready` or `the draft avoids a
902
+ generic pitch` is not enough. The user must see the actual subject, the
903
+ actual tokenized message template, one filled sample prospect version with
904
+ its token fill basis, your take, and the suggested adjustment before any
905
+ `approve-message` / `revise-messaging` question appears. If you catch
906
+ yourself wanting to offer `show me message` as a choice, stop: render the
907
+ slim chat review first, then ask only `approve-message` or
908
+ `revise-messaging`. Include `Open artifacts:` links to `message-review.md`
909
+ and `message-validation.md` before the approval question.
899
910
  - `My take:` and `Suggested adjustment:` are mandatory customer-facing decision
900
911
  fields, not optional summary text. They must appear after `Token notes:` and
901
912
  before the question in every rendered message review. They must each be
@@ -605,25 +605,23 @@
605
605
  "subject",
606
606
  "tokenized template",
607
607
  "sample prospect fill",
608
- "rendered examples",
609
- "bad token examples",
610
- "token notes",
611
608
  "my take",
612
609
  "suggested adjustment",
613
610
  "question",
614
611
  "recommendation"
615
612
  ],
616
613
  "mustRenderInlineBeforeQuestion": true,
617
- "minimumVisibleMessageDetail": "full_message_review_before_question",
614
+ "minimumVisibleMessageDetail": "tokenized_template_plus_sample_fill_only",
618
615
  "requiredArtifactLinks": [
619
616
  "message-review.md",
620
617
  "message-validation.md"
621
618
  ],
622
619
  "artifactLinkTiming": "before_approval_question",
623
- "requiredLabels": [
620
+ "requiredFileLabels": [
624
621
  "Subject:",
625
622
  "Tokenized template:",
626
623
  "Sample prospect fill:",
624
+ "Token fill basis:",
627
625
  "Rendered examples:",
628
626
  "Good token fill:",
629
627
  "Good omit:",
@@ -636,6 +634,18 @@
636
634
  "Question: approve-message or revise-messaging?",
637
635
  "Recommendation:"
638
636
  ],
637
+ "requiredChatLabels": [
638
+ "Status: message-review",
639
+ "Subject:",
640
+ "Tokenized template:",
641
+ "Sample prospect fill:",
642
+ "Token fill basis:",
643
+ "My take:",
644
+ "Suggested adjustment:",
645
+ "Question: approve-message or revise-messaging?",
646
+ "Recommendation:"
647
+ ],
648
+ "chatRenderRule": "Show ONLY the tokenized template and one filled sample prospect version (with Token fill basis), then My take, Suggested adjustment, Question, Recommendation, and Open artifacts links. Do NOT inline Rendered examples, Good token fill, Good omit, Bad token fill, Why bad, Fallback if missing, or Token notes — those live in message-review.md and message-validation.md only.",
639
649
  "allowedRecommendations": ["approve-message", "revise-messaging"],
640
650
  "judgeAgainst": [
641
651
  "generate-messages rules",
@@ -670,13 +680,7 @@
670
680
  "Subject:",
671
681
  "Tokenized template:",
672
682
  "Sample prospect fill:",
673
- "Rendered examples:",
674
- "Good token fill:",
675
- "Good omit:",
676
- "Bad token fill:",
677
- "Why bad:",
678
- "Fallback if missing:",
679
- "Token notes:",
683
+ "Token fill basis:",
680
684
  "My take:",
681
685
  "Suggested adjustment:",
682
686
  "Recommendation:"
@@ -1,120 +1,82 @@
1
1
  ---
2
2
  name: research-sender
3
- description: Enrichment-first sender research protocol. 0-3 agents, 30s target.
3
+ description: Parallel-first sender research protocol. One round of batched tool calls, ~10-15s wall time.
4
4
  visibility: internal
5
5
  ---
6
6
 
7
- # Research Sender (Enrichment-First)
7
+ # Research Sender (Parallel-First)
8
8
 
9
9
  Use this subskill when creating campaigns and you need sender/company research.
10
10
  This protocol is standalone and does not depend on the generic `research` subskill.
11
11
 
12
- ## Core Architecture
12
+ ## Core Idea — Don't Wait, Fan Out
13
13
 
14
- `enrich_sender` compact mode already returns `companySnapshot`, `senderBackground`, and `proofDigest`.
15
- Four of the old five research agents duplicated this data. Only Growth Signals + Case Studies are net-new.
16
- When `companySnapshot.description` is missing/thin, we also run a tiny positioning snippet pass (1 WebSearch) so we don't end up with blank company context.
14
+ Old protocol: call `enrich_sender` wait for partial maybe spawn 2-3 subagents → wait.
15
+ That's 3-4 sequential rounds and ~60s+ wall time.
17
16
 
18
- **This protocol uses enrichment as the baseline and only spawns extra work when proof or positioning context is missing.**
17
+ New protocol: **fire one parallel batch.** `enrich_sender` (5-8s, returns
18
+ `clientProspectId` + baseline) runs alongside 2-3 `WebSearch` calls. They all
19
+ finish in the same ~10s window, then you synthesize.
20
+
21
+ The `enrich_sender` call is still required because downstream tools
22
+ (`create_campaign`, `attach_sequence`, message generation) need the
23
+ `clientProspectId` it produces and the cached `EnrichedProspect` row it writes.
24
+ Don't skip it — just stop waiting for it before doing the rest of the research.
19
25
 
20
26
  ## Inputs
21
27
 
22
28
  Provide as many as available. Use `"Unknown"` for missing values.
23
29
 
30
+ - `linkedinUrl` (REQUIRED)
24
31
  - `name`
25
32
  - `title`
26
33
  - `companyName`
27
- - `companyDomain`
28
- - `companyLinkedinUrl` (optional)
29
- - `linkedinUrl` (optional)
34
+ - `companyDomain` (REQUIRED for WebSearch — derive from email or LinkedIn if absent)
30
35
  - `headline` (optional)
31
- - `enrichSender` response (REQUIRED — must include `companySnapshot`, `senderBackground`, `proofDigest`)
32
-
33
- ## Depth Decision (Deterministic — from proofDigest)
34
-
35
- Check the `proofDigest` from the `enrich_sender` compact response:
36
-
37
- ### `minimal-verification` (0-1 agent, 0-1 web calls, ~2-10 seconds)
38
-
39
- **Condition:** `proofDigest.caseStudyCount > 0` AND `proofDigest.reviewHighlight` exists (not null)
40
-
41
- - Do NOT spawn proof/growth agents (Case Studies, Growth Signals).
42
- - If `companySnapshot.description` is missing/thin, spawn the **Company Positioning** agent (1 WebSearch, no WebFetch).
43
- - Synthesize directly from enrichment data (companySnapshot + senderBackground + proofDigest) plus Agent 3 if used.
44
- - Call `complete_sender_research()` immediately with enrichment counts.
45
- - ~0 additional tokens.
46
-
47
- ### `deep-proof` (2-3 agents in parallel, max 5 tool calls total, ~30 seconds)
48
36
 
49
- **Condition:** `proofDigest.caseStudyCount === 0` OR `proofDigest.reviewHighlight` is null
37
+ ## The One Round (Mandatory Parallel Batch)
50
38
 
51
- - Spawn exactly **2 agents in parallel** (Case Studies + Growth Signals).
52
- - If `companySnapshot.description` is missing/thin, also spawn **1 positioning agent** (Company Positioning) in parallel.
53
- - Homepage, Company LinkedIn, Reviews all covered by enrichment, skip these entirely.
54
- - ~100k tokens total.
39
+ Issue ALL of the following in a single tool-call batch (Task subagents,
40
+ `multi_tool_use.parallel`, or whatever batching primitive your host exposes).
41
+ Do NOT issue any of them sequentially that defeats the protocol.
55
42
 
56
- ## Positioning Context (Conditionalfrom companySnapshot.description)
43
+ 1. `enrich_sender({ linkedinUrl, companyDomain })` baseline + `clientProspectId`.
44
+ 2. `WebSearch`: `site:{companyDomain} ("case study" OR "customer story" OR testimonial OR "success story") "{companyName}"` — proof.
45
+ 3. `WebSearch`: `"{companyName}" {companyDomain} {currentYear} (funding OR raised OR seed OR series OR hiring OR launch OR "press release")` — growth/credibility.
46
+ 4. `WebSearch` (conditional, include unless you have strong positioning context already): `"{companyName}" about product site:{companyDomain}` — positioning.
57
47
 
58
- If `companySnapshot.description` is null/undefined or shorter than ~120 characters, run the **Company Positioning** agent (1 WebSearch, no WebFetch).
48
+ That's it. No subagent fan-out. No second round of enrichment polling.
59
49
 
60
- ## Execution Backend Routing
61
-
62
- Choose backend once:
63
-
64
- 1. If `Task` is available: use Task subagents in parallel (Claude host).
65
- 2. Else if `multi_tool_use.parallel` is available: batch independent tool calls (Codex host).
66
- 3. Else: run units sequentially.
67
-
68
- Never claim Task subagents were used when they were not.
69
-
70
- ## Agent Prompts (when needed)
71
-
72
- ### Agent 1: Case Studies + Proof (max 3 tool calls)
73
-
74
- ```json
75
- {
76
- "subagent_type": "general-purpose",
77
- "model": "haiku",
78
- "description": "Case studies and proof",
79
- "prompt": "Find 1-3 concrete proof items for {companyName} ({companyDomain}).\n\nUse only: WebSearch, WebFetch.\n\nTool budget:\n- 1 WebSearch\n- Up to 2 WebFetch (only if the page looks like a case study/testimonial with named customers or metrics)\n\nWebSearch query (run exactly once):\nsite:{companyDomain} (\"case study\" OR \"customer story\" OR testimonial OR results OR \"success story\") \"{companyName}\"\n\nWebFetch: pick up to 2 results that look most likely to contain named customers or metrics.\nIf a fetched page is JS-heavy/empty, skip it and use snippets.\nDo not do a second search.\n\nReturn EXACTLY:\n---\nPROOF_OPTIONS: [Up to 3 items. Each: customer + outcome/metric + timeframe if present; or 'None found']\nSOURCES: [Short source hints]\n---\nKeep under 120 words. No extra commentary."
80
- }
81
- ```
50
+ ## Synthesis (Single Pass After Batch Returns)
82
51
 
83
- ### Agent 2: Growth/Credibility Signals (max 1 tool call)
52
+ Merge the four results. Treat `enrich_sender` as the authoritative baseline
53
+ for company/sender facts; treat WebSearch as proof and signals.
84
54
 
85
- ```json
86
- {
87
- "subagent_type": "general-purpose",
88
- "model": "haiku",
89
- "description": "Growth and credibility signals",
90
- "prompt": "Find recent growth/credibility signals for {companyName} ({companyDomain}).\n\nUse only: WebSearch.\nTool budget: 1 WebSearch.\n\nWebSearch query (run exactly once):\n\"{companyName}\" {companyDomain} {currentYear} (funding OR raised OR seed OR series OR hiring OR \"we're hiring\" OR launch OR announcement OR \"press release\")\n\nReturn EXACTLY:\n---\nHIRING: [Notable signals or 'None found']\nFUNDING: [Round, amount, date or 'None found']\nPRESS: [Recent announcement or 'None found']\n---\nKeep under 90 words. No extra commentary."
91
- }
92
- ```
93
-
94
- ### Agent 3: Company Positioning (max 1 tool call)
55
+ ```markdown
56
+ ## {Company} - Sender Research
95
57
 
96
- ```json
97
- {
98
- "subagent_type": "general-purpose",
99
- "model": "haiku",
100
- "description": "Company positioning context",
101
- "prompt": "If the enrichment company description is missing/thin, pull a crisp positioning summary for {companyName} ({companyDomain}) using only search snippets.\n\nUse only: WebSearch.\nTool budget: 1 WebSearch.\n\nWebSearch query (run exactly once):\n\"{companyName}\" about product site:{companyDomain}\n\nReturn EXACTLY:\n---\nPOSITIONING: [1-2 sentences]\nICP: [1 short line, or 'Unknown']\nDIFFERENTIATORS: [Up to 3 short bullets, or 'Unknown']\nSOURCES: [Short source hints]\n---\nKeep under 90 words. No extra commentary."
102
- }
58
+ **Company Context:** [companySnapshot.description OR positioning WebSearch top result]
59
+ **Industry / Size:** [companySnapshot.industry + employeeRange]
60
+ **Positioning Notes:** [proofDigest.positioningOneLiner + keyDifferentiators]
61
+ **Proof Options:** [proofDigest.caseStudySummary + case-study WebSearch hits — pick 1-3 with named customers / metrics]
62
+ **Credibility Signals:** [proofDigest.reviewHighlight + growth WebSearch hits (funding, hiring, press)]
63
+ **Gaps:** [what's still missing usually fine to ship with this]
103
64
  ```
104
65
 
105
- ## Synthesis (Orchestrator No Agents)
66
+ If `enrich_sender` returns `enrichmentStatus: "partial"`, that's expected and
67
+ fine — `companySnapshot` and `senderBackground` are populated immediately.
68
+ `proofDigest` may be null on partial; in that case rely on WebSearch results
69
+ for proof and credibility, not on a second enrichment poll.
106
70
 
107
- After agents return (or immediately for minimal-verification), merge enrichment baseline + agent findings:
71
+ ## Optional Deepen (Only If Synthesis Reveals A Hard Gap)
108
72
 
109
- ```markdown
110
- ## {Company} - Sender Research
73
+ If after synthesis you genuinely have zero proof and zero credibility
74
+ signals, AND the campaign fixture/operator told you proof is required, you
75
+ MAY issue ONE additional WebFetch on the most promising case-study URL from
76
+ batch step 2. Cap at one WebFetch. No subagents, no second WebSearch round.
111
77
 
112
- **Company Context:** [companySnapshot.description OR Agent 3 POSITIONING] + [companySnapshot.industry + employeeRange]
113
- **Positioning Notes:** [proofDigest.positioningOneLiner + keyDifferentiators + Agent 3 DIFFERENTIATORS (if present)]
114
- **Proof Options:** [proofDigest.caseStudySummary + agent upgrades if deep-proof]
115
- **Credibility Signals:** [proofDigest.reviewHighlight + agent growth signals if deep-proof]
116
- **Gaps:** [what's missing]
117
- ```
78
+ If that still yields nothing, set `proofItemsFound: 0` in
79
+ `complete_sender_research` and proceed; the brief can ship without proof.
118
80
 
119
81
  ## Completion Marker (Required)
120
82
 
@@ -124,7 +86,7 @@ After synthesis, call:
124
86
 
125
87
  Count rules:
126
88
 
127
- - `depth`: "minimal-verification" or "deep-proof"
89
+ - `depth`: always `"parallel-batch"` for this protocol.
128
90
  - `proofItemsFound`: number of concrete proof items you would confidently use.
129
91
  - `caseStudyItemsFound`: number of usable case-study examples.
130
92
  - `credibilitySignalsFound`: number of trust signals (reviews/ratings/funding/hiring/press).
@@ -133,26 +95,21 @@ If no reliable evidence is found, set counts to 0 and include that in `notes`.
133
95
 
134
96
  ## Progress UX
135
97
 
136
- If launching deep-proof agents:
98
+ Before issuing the parallel batch:
137
99
 
138
100
  ```
139
- Running a quick research pass for case studies and growth signals (~30s)...
101
+ Pulling sender enrichment + case studies + growth signals + positioning in parallel (~10s)...
140
102
  ```
141
103
 
142
- If also launching the Company Positioning agent:
104
+ After synthesis:
143
105
 
144
106
  ```
145
- Running a quick research pass for case studies, growth signals, and positioning (~30s)...
107
+ Sender research ready {proofItemsFound} proof items, {credibilitySignalsFound} credibility signals.
146
108
  ```
147
109
 
148
- If only launching the Company Positioning agent:
110
+ ## What Changed From The Previous Protocol
149
111
 
150
- ```
151
- Pulling a quick positioning summary from search snippets (~10s)...
152
- ```
153
-
154
- If skipping agents (minimal-verification):
155
-
156
- ```
157
- Enrichment already has strong proof — synthesizing directly from baseline data.
158
- ```
112
+ - No "minimal-verification vs deep-proof" branch. Always run the same parallel batch.
113
+ - No spawn of `general-purpose` subagents for research — WebSearch from the orchestrator turn is faster and cheaper.
114
+ - No waiting on `enrich_sender` before issuing WebSearch.
115
+ - One round, one synthesis, one `complete_sender_research` call.