@sellable/mcp 0.1.90 → 0.1.92

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
@@ -248,9 +248,10 @@ Parallel execution contract:
248
248
  use that registry to launch the filter-leads scout and message-generation
249
249
  scout together when the host supports real subagents.
250
250
  - Claude host: use the installed `source-scout-linkedin-engagement`,
251
- `source-scout-sales-nav`, and `source-scout-prospeo-contact` Task/Agent subagents
252
- for parallel lead-source scouting. Launch all credible lanes in one assistant
253
- message so Claude Code can run them concurrently/background.
251
+ `source-scout-sales-nav`, and `source-scout-prospeo-contact` Task/Agent
252
+ subagents for parallel lead-source scouting only when the current session
253
+ exposes those agent names. Launch all credible lanes in one assistant message
254
+ so Claude Code can run them concurrently/background.
254
255
  - Codex host: use named custom scouts for source-angle work when subagents are
255
256
  available: `source-scout-linkedin-engagement`, `source-scout-sales-nav`, and
256
257
  `source-scout-prospeo-contact`. Use `multi_tool_use.parallel` for independent
@@ -258,8 +259,9 @@ Parallel execution contract:
258
259
  batched lookups.
259
260
  - For the post-lead stage, Codex/Claude should launch
260
261
  `post-find-leads-filter-scout` and `post-find-leads-message-scout` in the
261
- same turn/message when subagents are available. If not, run them sequentially
262
- and do not claim parallel work.
262
+ same turn/message when those subagents are available in the current session.
263
+ If not, run them sequentially with MCP tools/assets, do not surface install
264
+ status to the customer, and do not claim parallel work.
263
265
  - If neither backend is available, run sequentially with the same output schema.
264
266
 
265
267
  Config path resolution (in order):
@@ -18,11 +18,13 @@ Required inputs:
18
18
  Required first steps:
19
19
 
20
20
  1. Read the three required inputs.
21
- 2. Load 100% of the real generate-messages prompt with chunked
22
- `get_subskill_prompt({ subskillName: "generate-messages", offset, limit })`
23
- calls until `hasMore` is false.
24
- 3. Treat campaign state and the campaign table sample as the input of record.
21
+ 2. Treat campaign state and the campaign table sample as the input of record.
25
22
  Disk files are context/debug aids, not durable state.
23
+ 3. Use the embedded Message Review Safety Gate below. Do not load the full
24
+ long-form `generate-messages` subskill in this `create-campaign-v2` path. If
25
+ a needed rule is missing or the draft fails quality gates, return
26
+ `revise-messaging` with the exact failure instead of pulling the long prompt
27
+ into this worker.
26
28
 
27
29
  Owned outputs:
28
30
 
@@ -44,13 +46,13 @@ Do not write or modify:
44
46
 
45
47
  Process:
46
48
 
47
- 1. Run the loaded generate-messages workflow in dry mode from the approved
48
- brief, lead-review source decision, and `lead-sample.json`.
49
+ 1. Run the embedded message-review safety-gate workflow in dry mode from the
50
+ approved brief, lead-review source decision, and `lead-sample.json`.
49
51
  2. Use `lead-sample.json` as the only lead sample source. Do not fetch new
50
52
  prospects or invent richer row signals.
51
53
  3. Build proof inventory, token fill rules, token adherence, angle drafts,
52
- kill/combine review, finalists, skeptical-prospect review, winner gate, and
53
- a raw sendable selected winner.
54
+ kill/combine review, skeptical-prospect review, winner gate, and a raw
55
+ sendable selected winner.
54
56
  4. If `lead-filter.md` already exists, cite only basis rows that pass it. If it
55
57
  does not exist yet, choose probable good-fit rows from `lead-sample.json` and
56
58
  mark the final reconciliation as pending.
@@ -67,7 +69,8 @@ Process:
67
69
  Return a concise final status with:
68
70
 
69
71
  - artifacts written
70
- - whether the full generate-messages prompt was loaded
72
+ - whether the embedded message-review safety-gate rules passed or returned
73
+ `revise-messaging`
71
74
  - lead sample basis used
72
75
  - proposed template and one sample message
73
76
  - selected winner summary
@@ -76,7 +79,56 @@ Return a concise final status with:
76
79
  Quality bar:
77
80
 
78
81
  - Do not synthesize a lightweight message from general knowledge. The artifact
79
- must prove the full generate-messages workflow ran.
82
+ must prove the embedded message-review safety-gate workflow ran.
80
83
  - Message generation can start before `lead-filter.md`, but message review
81
84
  cannot start until the parent verifies the selected basis rows still pass the
82
85
  final filter.
86
+
87
+ ## Embedded Message Review Safety Gate
88
+
89
+ Use this campaign-launch subset to produce a truthful first-send message,
90
+ rendered token examples, and a decision without loading the full long-form
91
+ message prompt.
92
+
93
+ Required `message-validation.md` sections:
94
+
95
+ - `Status`
96
+ - `Mode`
97
+ - `Lead Sample Basis`
98
+ - `Strongest Reply Reason`
99
+ - `Campaign Element Pool`
100
+ - `Gold Standard Strategy Map`
101
+ - `Current Campaign Translation`
102
+ - `Token Fill Rules`
103
+ - `Token Adherence Table`
104
+ - `Angle Drafts`
105
+ - `Kill / Combine Review`
106
+ - `Finalizer Pass`
107
+ - `Gold-Standard Quality Gate`
108
+ - `Skeptical Prospect Review`
109
+ - `Winner Gate`
110
+ - `Selected Winner`
111
+ - `Findings`
112
+ - `Recommendation`
113
+
114
+ Quality gates:
115
+
116
+ - The selected winner is one first outbound send only. No post-accept DM,
117
+ follow-up, cadence branch, or sequence copy.
118
+ - Explain what the product is and what it does in plain language before asking
119
+ for a call.
120
+ - Use only proof from the brief, source review, sample, campaign state, or
121
+ explicit user answers. Unsupported reply-rate, meeting-rate, ROI, revenue,
122
+ and customer-logo claims are blocked.
123
+ - Include at least one supported `{{token}}` when templating, plus a complete
124
+ rendered good-fill example and a complete rendered omit/fallback example.
125
+ - Do not use internal tokens such as `{{profile_signal}}` in customer-facing
126
+ copy.
127
+ - Do not put bracketed instructions in the message body, such as `[ROW_BRIDGE]`,
128
+ `[insert]`, or `[generated]`.
129
+ - Optional row-specific personalization must be grounded in a row field or
130
+ omitted entirely.
131
+ - Subjects should be short, buyer-relevant, and specific. Avoid `quick
132
+ question`, `demo`, `founder call`, sender names, and generic `outbound`.
133
+ - If the message is plausible but not ready to send, set `Recommendation:
134
+ revise-messaging`.
@@ -26,6 +26,17 @@ function buildWatchUrl(config, redirect) {
26
26
  : "";
27
27
  return `${config.apiUrl}/auth/continue?token=${config.token}&redirect=${redirect}${workspaceParam}`;
28
28
  }
29
+ function isLinkedInProfileUrl(input) {
30
+ try {
31
+ const url = new URL(input);
32
+ const host = url.hostname.toLowerCase();
33
+ return ((host === "linkedin.com" || host.endsWith(".linkedin.com")) &&
34
+ /^\/in\/[^/]+\/?$/i.test(url.pathname));
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
29
40
  export const campaignToolDefinitions = [
30
41
  {
31
42
  name: "get_campaigns",
@@ -613,9 +624,10 @@ export async function createCampaign(input) {
613
624
  if (!hasClientProspectId && !senderLinkedinUrl) {
614
625
  missing.push("clientProspectId or senderLinkedinUrl");
615
626
  }
616
- // Cheap URL sanity check on senderLinkedinUrl when supplied.
617
- if (senderLinkedinUrl && !senderLinkedinUrl.includes("linkedin.com")) {
618
- throw new Error("VALIDATION_ERROR: senderLinkedinUrl must be a LinkedIn URL (must contain 'linkedin.com'). Got: " +
627
+ // Cheap URL sanity check on senderLinkedinUrl when supplied. This input is a
628
+ // profile bootstrap fallback; company pages should use clientProspectId.
629
+ if (senderLinkedinUrl && !isLinkedInProfileUrl(senderLinkedinUrl)) {
630
+ throw new Error("VALIDATION_ERROR: senderLinkedinUrl must be a LinkedIn profile URL like https://www.linkedin.com/in/name/. Company pages require clientProspectId or a discovered profile URL. Got: " +
619
631
  senderLinkedinUrl);
620
632
  }
621
633
  if (missing.length > 0) {
@@ -16,6 +16,14 @@ export function markResearchPromptLoaded(mode = "generic", subskillName) {
16
16
  : mode === "prospect"
17
17
  ? "research-prospect"
18
18
  : "research");
19
+ const existing = researchPromptStates[mode];
20
+ // Re-loading the same research prompt is a context-management detail, not
21
+ // new research. Preserve the original load timestamp so a completed research
22
+ // marker does not become stale just because the host refreshed instructions.
23
+ if (existing?.subskillName === normalizedSubskillName) {
24
+ latestResearchPromptState = existing;
25
+ return existing;
26
+ }
19
27
  const state = {
20
28
  loadedAt: new Date().toISOString(),
21
29
  mode,
@@ -132,7 +132,7 @@ export const promptToolDefinitions = [
132
132
  },
133
133
  {
134
134
  name: "get_source_scout_registry",
135
- description: "Return the canonical Sellable source-scout agent names and host filenames. Use this before lead-source scouting so Codex and Claude launch the same named scouts from one registry.",
135
+ description: "Return the canonical Sellable source-scout agent names and host filenames. Use this before lead-source scouting so Codex and Claude can launch the same named scouts from one registry when the current host exposes them.",
136
136
  inputSchema: {
137
137
  type: "object",
138
138
  properties: {},
@@ -242,9 +242,9 @@ export function getSourceScoutRegistry() {
242
242
  legacy: agent.legacy,
243
243
  })),
244
244
  usage: {
245
- codex: "Spawn credible scouts by the returned `name` values in one assistant turn when the user approved background source scouts.",
246
- claude: "Invoke Claude Code Task/Agent subagents with subagent_type equal to the returned `name` values.",
247
- parentThreadRule: "Do not preload every provider prompt in the parent; each scout loads only the provider prompt for its lane.",
245
+ codex: "Spawn credible scouts by the returned `name` values in one assistant turn only when the current Codex host exposes those custom agents.",
246
+ claude: "Invoke Claude Code Task/Agent subagents with subagent_type equal to the returned `name` values only when the current Claude session lists those agents.",
247
+ parentThreadRule: "Named agents are optional acceleration. If they are absent, do not customer-surface install status; run the same provider probes with MCP tools from the parent thread. Do not preload every provider prompt before spawning agents; each scout loads only the provider prompt for its lane.",
248
248
  },
249
249
  };
250
250
  }
@@ -287,9 +287,9 @@ export function getPostFindLeadsScoutRegistry() {
287
287
  nextStage: "message-review",
288
288
  },
289
289
  usage: {
290
- codex: "After the user approves or auto-confirms the lead source, spawn both returned scout `name` values in one assistant turn when subagents are available.",
291
- claude: "After lead source approval, invoke both returned Task/Agent subagents in one assistant message so filter-leads and message generation run concurrently.",
292
- parentThreadRule: "Do not run filter-leads to completion before starting message generation. Both scouts consume brief.md, lead-review.md, and lead-sample.json; join before message review.",
290
+ codex: "After the user approves or auto-confirms the lead source, spawn both returned scout `name` values in one assistant turn only when the current Codex host exposes those custom agents.",
291
+ claude: "After lead source approval, invoke both returned Task/Agent subagents in one assistant message only when the current Claude session lists those agents, so filter-leads and message generation run concurrently.",
292
+ parentThreadRule: "Named agents are optional acceleration. If they are absent, do not customer-surface install status; the main thread still orchestrates filter and message branches from brief.md, lead-review.md, and lead-sample.json. The message branch should use the baked-in message-scout prompt or the create-campaign-v2 message-review safety gate asset, not the full long generate-messages prompt in the normal path. Join before message review.",
293
293
  },
294
294
  };
295
295
  }
@@ -39,9 +39,18 @@ type WaitForRubricResultsInput = {
39
39
  type WorkflowTableStats = {
40
40
  tableId: string;
41
41
  totalRows: number;
42
+ enrichedCount?: number;
43
+ needsEnrichCount?: number;
44
+ messagesCount?: number;
45
+ needsApprovalCount?: number;
46
+ processingCount?: number;
47
+ failedCount?: number;
42
48
  passRate?: {
43
49
  completed: number;
44
50
  passed: number;
51
+ pending?: number;
52
+ percent?: number;
53
+ targetCount?: number;
45
54
  };
46
55
  };
47
56
  export declare const rubricToolDefinitions: ({
@@ -491,8 +500,12 @@ export declare function waitForRubricResults(input: WaitForRubricResultsInput):
491
500
  passed: number;
492
501
  percent: number;
493
502
  targetCount: number;
503
+ pending?: undefined;
494
504
  };
495
505
  reason?: undefined;
506
+ partialResult?: undefined;
507
+ diagnostic?: undefined;
508
+ guidance?: undefined;
496
509
  } | {
497
510
  ready: boolean;
498
511
  reason: string;
@@ -502,9 +515,28 @@ export declare function waitForRubricResults(input: WaitForRubricResultsInput):
502
515
  passRate: {
503
516
  completed: number;
504
517
  passed: number;
518
+ pending: number;
505
519
  percent: number;
506
520
  targetCount: number;
507
521
  };
522
+ partialResult: {
523
+ completed: number;
524
+ passed: number;
525
+ pending: number;
526
+ percent: number;
527
+ targetCount: number;
528
+ enoughToDiagnose: boolean;
529
+ };
530
+ diagnostic: {
531
+ totalRows: number;
532
+ enrichedCount: number | undefined;
533
+ needsEnrichCount: number | undefined;
534
+ messagesCount: number | undefined;
535
+ needsApprovalCount: number | undefined;
536
+ processingCount: number | undefined;
537
+ failedCount: number | undefined;
538
+ };
539
+ guidance: string;
508
540
  stats: WorkflowTableStats | null;
509
541
  }>;
510
542
  export {};
@@ -332,7 +332,7 @@ export const rubricToolDefinitions = [
332
332
  },
333
333
  {
334
334
  name: "wait_for_rubric_results",
335
- description: "Wait for ICP rubric pass-rate results to complete for queued prospects.",
335
+ description: "Wait for ICP rubric pass-rate results to complete for queued prospects. On timeout, returns partial diagnostic stats; create-campaign-v2 should not poll indefinitely.",
336
336
  inputSchema: {
337
337
  type: "object",
338
338
  properties: {
@@ -672,6 +672,9 @@ export async function waitForRubricResults(input) {
672
672
  const completed = lastStats?.passRate?.completed ?? 0;
673
673
  const passed = lastStats?.passRate?.passed ?? 0;
674
674
  const percent = completed > 0 ? Math.round((passed / completed) * 100) : 0;
675
+ const totalRows = lastStats?.totalRows ?? 0;
676
+ const effectiveTarget = totalRows > 0 ? Math.min(targetCount, totalRows) : targetCount;
677
+ const pending = Math.max(effectiveTarget - completed, 0);
675
678
  return {
676
679
  ready: false,
677
680
  reason: "timeout",
@@ -681,9 +684,28 @@ export async function waitForRubricResults(input) {
681
684
  passRate: {
682
685
  completed,
683
686
  passed,
687
+ pending,
684
688
  percent,
685
- targetCount,
689
+ targetCount: effectiveTarget,
686
690
  },
691
+ partialResult: {
692
+ completed,
693
+ passed,
694
+ pending,
695
+ percent,
696
+ targetCount: effectiveTarget,
697
+ enoughToDiagnose: completed > 0,
698
+ },
699
+ diagnostic: {
700
+ totalRows,
701
+ enrichedCount: lastStats?.enrichedCount,
702
+ needsEnrichCount: lastStats?.needsEnrichCount,
703
+ messagesCount: lastStats?.messagesCount,
704
+ needsApprovalCount: lastStats?.needsApprovalCount,
705
+ processingCount: lastStats?.processingCount,
706
+ failedCount: lastStats?.failedCount,
707
+ },
708
+ guidance: "If this is create-campaign-v2 validate-sample, do not repeat waits indefinitely. Use passRate/stats to diagnose and surface sample_revision_required before Settings when the sample is under the pass floor or messages are incomplete.",
687
709
  stats: lastStats,
688
710
  };
689
711
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.90",
3
+ "version": "0.1.92",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -120,23 +120,28 @@ scout those angles as independent branches when the host can actually do it:
120
120
  LinkedIn Engagement / active post engagers (internal `signal-discovery`
121
121
  provider prompt), Sales Nav / title + company filters, and Prospeo Contact /
122
122
  domains only when relevant. In Codex, explicitly spawn the named custom scouts
123
- `source-scout-linkedin-engagement`, `source-scout-sales-nav`, and `source-scout-prospeo-contact` for
124
- the credible lanes; Codex does not infer subagent fan-out from generic source
123
+ `source-scout-linkedin-engagement`, `source-scout-sales-nav`, and
124
+ `source-scout-prospeo-contact` for the credible lanes only when the current host
125
+ exposes those names; Codex does not infer subagent fan-out from generic source
125
126
  comparison wording. In Claude Code, invoke the generated `source-scout-*`
126
- Task/Agent subagents for all credible lanes in one assistant message; the
127
- installer writes them from the same canonical Sellable agent registry with
128
- explicit Sellable MCP tool allowlists. The create-campaign-v2 subskill calls
129
- `get_source_scout_registry` before dispatch so the current registry, not this
130
- copy, is the runtime source of truth. If the host runs them sequentially, do not
131
- claim they ran in parallel. In chat, call the downstream copy stage `message generation`;
127
+ Task/Agent subagents for all credible lanes in one assistant message only when
128
+ the current session lists those names. The installer writes them from the same
129
+ canonical Sellable agent registry with explicit Sellable MCP tool allowlists.
130
+ The create-campaign-v2 subskill calls `get_source_scout_registry` before
131
+ dispatch so the current registry, not this copy, is the runtime source of truth.
132
+ If the host runs them sequentially or the named agents are unavailable, do not
133
+ claim they ran in parallel and do not surface install status to the customer. In
134
+ chat, call the downstream copy stage `message generation`;
132
135
  `message-validation.md` is only an internal proof artifact.
133
136
 
134
137
  After find-leads returns a lead source and the user approves it, use the same
135
138
  registry pattern for the two post-lead branches. The create-campaign-v2 subskill
136
139
  calls `get_post_find_leads_scout_registry`, then launches the returned
137
140
  filter-leads scout and message-generation scout together when real subagents are
138
- available. Message generation must not wait for the filter unless the host
139
- cannot run the two branches concurrently.
141
+ available and the current session exposes the returned names. Message
142
+ generation must not wait for the filter unless the host cannot run the two
143
+ branches concurrently. If the post-lead agents are absent, the main thread still
144
+ orchestrates the same branches from the compact context with MCP tools/assets.
140
145
 
141
146
  Use rendered Markdown for user review surfaces, not fenced code blocks. Keep
142
147
  lines short, use indexed section labels and bullets, and translate internal
@@ -218,18 +223,20 @@ Treat host capabilities as concrete functions, not prose conventions:
218
223
  `mcp__sellable__get_post_find_leads_scout_registry({})` after source
219
224
  approval and before dispatching the post-lead filter/message scouts.
220
225
  - `launch_source_scout`: Claude Code uses `Task` with `subagent_type` equal to
221
- the registry `name`; Codex uses named custom agents such as
226
+ the registry `name` only when the session lists those agents; Codex uses
227
+ named custom agents such as
222
228
  `source-scout-linkedin-engagement`, `source-scout-sales-nav`, and
223
229
  `source-scout-prospeo-contact` when subagents are available.
224
230
  - `launch_post_find_leads_scout`: Claude Code uses `Task` with `subagent_type`
225
- equal to the returned post-lead registry `name`; Codex uses the returned
226
- custom agents such as `post-find-leads-filter-scout` and
227
- `post-find-leads-message-scout` when subagents are available.
231
+ equal to the returned post-lead registry `name` only when the session lists
232
+ those agents; Codex uses the returned custom agents such as
233
+ `post-find-leads-filter-scout` and `post-find-leads-message-scout` when
234
+ subagents are available.
228
235
 
229
- If a required interactive host function is missing, stop and explain the
230
- Sellable install/reload problem. Do not silently simulate structured choices,
231
- subprompt loading, source-scout dispatch, or post-lead scout dispatch with local
232
- scripts.
236
+ If a required interactive question function or MCP loader is missing, stop and
237
+ explain the Sellable install/reload problem. Named scout agents are optional
238
+ acceleration: if they are absent, do not surface install status to the customer;
239
+ fall back to product-native MCP orchestration instead of local scripts.
233
240
 
234
241
  Never narrate local draft housekeeping to the user. If you create directories,
235
242
  save drafts, write artifacts, or persist intermediate state, translate it into
@@ -480,16 +487,20 @@ updates.
480
487
  copies of this file; packaged Claude Code and Codex runs must use the MCP
481
488
  asset loader so they share the same config.
482
489
  3. Follow that prompt and workflow config exactly.
483
- 4. For message generation, load the full `generate-messages` prompt in the
484
- same run with chunked
485
- `mcp__sellable__get_subskill_prompt({ subskillName: "generate-messages", offset, limit })`
486
- calls until `hasMore` is false. Do not synthesize
487
- `message-validation.md` from the brief, lead review, or general knowledge.
490
+ 4. For message generation, use the `post-find-leads-message-scout` agent when
491
+ available; its prompt carries the campaign-launch message rules. In the
492
+ parent-thread fallback, load
493
+ `mcp__sellable__get_subskill_asset({ subskillName: "create-campaign-v2", assetPath: "references/message-review-safety-gate.md" })`.
494
+ Do not load the full long-form `generate-messages` subskill in this
495
+ create-campaign path; if the safety gate cannot safely approve the draft,
496
+ route to revise messaging with the concrete failure.
497
+ Do not synthesize `message-validation.md` from the brief, lead review, or
498
+ general knowledge.
488
499
  5. Create the campaign shell early with the v1 brief so the user can open the
489
500
  watch link and see useful setup state immediately. Import only the first
490
501
  bounded review batch after the source is attached to the campaign; do not
491
502
  queue workflow cells, attach a sequence, or start until
492
- `message-validation.md` proves the full generate-messages workflow ran,
503
+ `message-validation.md` proves the message-review safety-gate workflow ran,
493
504
  `message-review.md` approves the template, rubrics are saved, and the
494
505
  approved message set is synced into the campaign brief.
495
506
  6. Keep `selectedLeadListId` as the source list and `workflowTableId` as the
@@ -189,7 +189,10 @@ Validated draft directory:
189
189
  it as the preferred `create_campaign` identity input. If it includes a
190
190
  LinkedIn profile URL, keep that URL as `senderLinkedinUrl` so the backend can
191
191
  resolve/materialize the sender prospect when the watchable campaign shell is
192
- created. Do not require a connected sender before shell creation.
192
+ created. Do not pass a LinkedIn company page as `senderLinkedinUrl`; for a
193
+ company-only identity, use `clientProspectId` if already materialized or do
194
+ one lightweight lookup to find the relevant founder/operator profile. Do not
195
+ require a connected sender before shell creation.
193
196
  - If the user supplied a LinkedIn profile, website, domain, company name, or
194
197
  explicit client prospect identity in the invocation, do one lightweight lookup
195
198
  first:
@@ -217,7 +220,8 @@ Validated draft directory:
217
220
  profile, call `fetch_linkedin_profile` and infer the current or most recent
218
221
  company from the profile. For a company website, call `fetch_company` when
219
222
  possible, otherwise one web lookup. If a LinkedIn profile URL is available,
220
- retain it as `senderLinkedinUrl` for `create_campaign`; if a
223
+ retain it as `senderLinkedinUrl` for `create_campaign`; if only a LinkedIn
224
+ company page is available, do not pass it as `senderLinkedinUrl`. If a
221
225
  `clientProspectId` is available, pass that instead.
222
226
 
223
227
  After the user confirms the campaign identity, check whether the company
@@ -368,11 +372,12 @@ Validated draft directory:
368
372
  not "looks good".
369
373
  Approval options should refer to what the user just read, e.g. `Approve this
370
374
  brief`, `Revise target`, `Revise offer/proof`, and `Other / custom`.
371
- When the user chose `Find people for me` and the host supports subagents,
372
- make the recommended approval label explicit: `Approve brief + use background
373
- source scouts`. That gives Codex/Claude a user-visible request to launch
374
- source-scout subagents after approval instead of silently falling back to
375
- parent-thread provider probes.
375
+ When the user chose `Find people for me` and the current runtime exposes the
376
+ named Sellable source-scout agents, make the recommended approval label
377
+ explicit: `Approve brief + use background source scouts`. If those agents are
378
+ not visible in this session, use `Approve brief + compare source paths`
379
+ instead. The customer approves the sourcing decision, not the host
380
+ implementation detail.
376
381
  Include an `Open artifact:` link to `brief.md` before the approval question.
377
382
  The visible brief must come before local persistence chrome. After the brief
378
383
  is synthesized, render the approval-ready brief in chat before running visible
@@ -402,7 +407,7 @@ campaign table, then use that same batch to tighten the fit filter and draft the
402
407
  message we should test.`
403
408
  - During long post-intake work, show concise progress checkpoints before the
404
409
  next expensive stage: source being checked, source switch/tradeoff if any,
405
- lead sample usable, filter/message drafting, and full message prompt loading.
410
+ lead sample usable, filter/message drafting, and message-review rule loading.
406
411
  Each checkpoint should include a rough remaining time when useful. If a step
407
412
  exceeds its estimate by roughly a minute or more, acknowledge it lightly once
408
413
  and explain why the extra care helps the campaign. Example:
@@ -423,6 +428,10 @@ message we should test.`
423
428
  - `brief.md` is the single user-facing debug copy of the brief. The
424
429
  `CampaignOffer.campaignBrief` field is the stable downstream thesis source.
425
430
  - `lead-review.md` and `lead-sample.json` are the required outputs of `find leads`.
431
+ `lead-review.md` is customer-readable: describe source paths as provider
432
+ lanes checked (Signals, Sales Nav, Prospeo) and never include custom agent
433
+ names, host availability, install state, allowed-tool state, or fallback
434
+ implementation details.
426
435
  - `lead-filter.md` is the primary output of `filter leads`.
427
436
  - `rubric.json` is optional and secondary to `lead-filter.md`.
428
437
  - `message-prep.md` is an optional speed artifact produced by the explicit
@@ -443,14 +452,18 @@ message we should test.`
443
452
  campaign table `workflowTableId` exist, the normal path is the
444
453
  `post-lead-workstreams` step. Launch `filter leads` and `message generation`
445
454
  from the same basis (`brief.md`, `lead-review.md`, `lead-sample.json`, and
446
- the imported review-batch rows) as separate workstreams when the host supports
447
- real subagents/background work.
455
+ the imported review-batch rows) as separate workstreams when the host exposes
456
+ the named Sellable post-find-leads agents or real background work.
448
457
  First call `get_post_find_leads_scout_registry` and use the returned
449
- canonical `name` values. In Claude Code, use both returned Task/Agent
450
- subagents in the same assistant message; in Codex, use both returned custom
451
- scouts as disjoint subagents in the same assistant turn when the host exposes
452
- them. The existing `filter-rubric` and `message-generation` steps remain
453
- focused retry and resume targets.
458
+ canonical `name` values only when those names are available in the current
459
+ runtime. In Claude Code, use both returned Task/Agent subagents in the same
460
+ assistant message when the session lists them; in Codex, use both returned
461
+ custom scouts as disjoint subagents in the same assistant turn when the host
462
+ exposes them. If the names are absent, the main thread still orchestrates the
463
+ same filter and message branches with MCP tools/assets from the same compact
464
+ context. Do not surface agent install status to the customer. The existing
465
+ `filter-rubric` and `message-generation` steps remain focused retry and
466
+ resume targets.
454
467
  - Message generation does not need `lead-filter.md` to start. The moment the
455
468
  bounded review batch is imported and `workflowTableId` is ready, start the
456
469
  message-generation workstream from `brief.md`, `lead-review.md`,
@@ -473,19 +486,23 @@ message we should test.`
473
486
  - Parallel means real parallel execution, not optimistic progress copy. For the
474
487
  lead-source scout, first call `get_source_scout_registry` and use the
475
488
  returned canonical `name` values. In Codex, explicitly spawn one named custom scout per
476
- credible source lane when subagents are available: `source-scout-linkedin-engagement`
489
+ credible source lane when those subagents are visible in the current runtime:
490
+ `source-scout-linkedin-engagement`
477
491
  (display: LinkedIn Engagement Scout, powered by the `signal-discovery`
478
492
  provider prompt), `source-scout-sales-nav` (Sales Nav Scout), and
479
- `source-scout-prospeo-contact` (Prospeo Contact Scout). For Claude Code, explicitly
480
- invoke the generated `source-scout-*` Task/Agent subagents installed by
481
- `@sellable/install` for all credible lanes in one assistant message; they are
482
- installed from the same canonical Sellable agent registry and carry explicit
483
- Sellable MCP tool allowlists. The parent thread should not preload every provider prompt before
484
- spawning scouts; each scout loads only its own provider prompt. If host
485
- subagents are unavailable, use independent MCP/tool calls
486
- in the same model turn or dedicated Sellable MCP tools that perform
487
- server-side `Promise.all` fan-out. If real parallel execution is not available
488
- or not allowed, run the same DAG sequentially and use honest copy: `I’ll tighten the filter first,
493
+ `source-scout-prospeo-contact` (Prospeo Contact Scout). For Claude Code,
494
+ invoke the generated `source-scout-*` Task/Agent subagents for all credible
495
+ lanes in one assistant message only when the session exposes those names.
496
+ They are installed from the same canonical Sellable agent registry and carry
497
+ explicit Sellable MCP tool allowlists when `@sellable/install` is active. The
498
+ parent thread should not preload every provider prompt before spawning scouts;
499
+ each scout loads only its own provider prompt. If host subagents are
500
+ unavailable or the names are not visible, use the same provider probes via
501
+ independent MCP/tool calls in the parent thread or dedicated Sellable MCP
502
+ tools that perform server-side `Promise.all` fan-out. Do not write "source
503
+ scout agents are not installed" or similar host status into customer-facing
504
+ output. If real parallel execution is not available or not allowed, run the
505
+ same DAG sequentially and use honest copy: `I’ll tighten the filter first,
489
506
  then draft the message from the same sample.` Never say `kicking off two
490
507
  workstreams`, `in parallel`, or `background` unless parallel branches were
491
508
  actually launched.
@@ -698,15 +715,19 @@ Required behavior:
698
715
  branches, then compare the outputs in `lead-review.md`. Call
699
716
  `get_source_scout_registry` first so new scouts can be added without prompt
700
717
  rewrites. In Codex, explicitly
701
- spawn named custom subagents in the same turn: `source-scout-linkedin-engagement`,
702
- `source-scout-sales-nav`, and `source-scout-prospeo-contact` for the credible lanes. Codex
703
- does not infer this from generic "compare paths" wording. If the realistic
718
+ spawn named custom subagents in the same turn when the host exposes them:
719
+ `source-scout-linkedin-engagement`, `source-scout-sales-nav`, and
720
+ `source-scout-prospeo-contact` for the credible lanes. Codex does not infer
721
+ this from generic "compare paths" wording. If the realistic
704
722
  source set is LinkedIn Engagement + Sales Nav (Signals + Sales Nav), run both.
705
723
  If it is LinkedIn Engagement + Prospeo Contact (Signals + Prospeo), run both.
706
724
  If all three are credible, run all three when the host/runtime supports it.
707
725
  In Claude Code, launch the matching `source-scout-linkedin-engagement`,
708
- `source-scout-sales-nav`, and/or `source-scout-prospeo-contact` Task/Agent subagents
709
- in the same assistant message, not as sequential scout turns.
726
+ `source-scout-sales-nav`, and/or `source-scout-prospeo-contact` Task/Agent
727
+ subagents in the same assistant message only when the current session lists
728
+ those names, not as sequential scout turns. If they are absent, run the same
729
+ provider probes with MCP tools from the parent thread and keep
730
+ `lead-review.md` focused on source evidence, not host-agent availability.
710
731
  - Branch A: LinkedIn Engagement / active LinkedIn posts (internal provider:
711
732
  Signals / `signal-discovery`). Search relevant keyword lanes, review
712
733
  finalist posts, fetch top-post engagers, and estimate warm-fit volume.
@@ -1042,17 +1063,29 @@ Do not:
1042
1063
 
1043
1064
  ## Step 3: Generate Message
1044
1065
 
1045
- Step 3 follows the canonical `generate-messages` subskill gold-standard examples, REPLY framework fallback, Proof Inventory, Token Fill Rules, Token Adherence Table, Angle Drafts, Kill/Combine Review, Finalists, Finalizer Pass, Gold-Standard Quality Gate, Skeptical Prospect Review, Winner Gate, and the message-review-decision contract. The `generate-messages` subskill is the authoritative source — this entry prompt does not duplicate it.
1046
-
1047
- When you reach Step 3, load the canonical subskill verbatim via chunked reads:
1066
+ Step 3 is orchestrated by the main thread and executed either by the
1067
+ `post-find-leads-message-scout` worker or by the parent-thread fallback. The
1068
+ worker carries the campaign-launch message rules in its agent prompt. The
1069
+ parent-thread fallback must load the small safety-gate reference before writing
1070
+ message artifacts:
1048
1071
 
1049
1072
  ```
1050
- mcp__sellable__get_subskill_prompt({ subskillName: "generate-messages", offset, limit })
1073
+ mcp__sellable__get_subskill_asset({ subskillName: "create-campaign-v2", assetPath: "references/message-review-safety-gate.md" })
1051
1074
  ```
1052
1075
 
1053
- Continue chunked-read until `hasMore=false`. Treat all chunks as one prompt load. Follow it verbatim once loaded. The same directive is locked in `flow.v2.json::messageGeneration.toolRules`.
1054
-
1055
- Do NOT proceed to Step 4 (message review gate) without loading and following the full generate-messages workflow. `message-validation.md` must prove the full workflow ran (all required sections present + raw sendable Selected Winner + explicit single-send-unit PASS) before `message-review.md` can recommend `approve-message`. If the message review is ready but the generate-messages prompt was not retrieved in this run, route back to message-generation before queueing the 10-lead test cascade. If `message-validation.md` contains post-accept DM, follow-up, second-touch, cadence, branch, or other sequence-shaped copy, do not summarize it as ready; route back to message-generation and require a single first outbound send.
1076
+ Do not load the full long-form `generate-messages` subskill in
1077
+ `create-campaign-v2`. The safety-gate asset is a parent-thread fallback for hosts
1078
+ that cannot launch `post-find-leads-message-scout`, not the preferred route. If
1079
+ the safety gate is missing a needed campaign-specific rule or the draft fails
1080
+ quality gates, stop at `revise-messaging` with the exact failure instead of
1081
+ pulling the long prompt into the main thread.
1082
+
1083
+ Do NOT proceed to Step 4 (message review gate) unless `message-validation.md`
1084
+ contains the safety-gate required sections, a raw sendable Selected Winner, and an
1085
+ explicit single-first-send PASS. If `message-validation.md` contains post-accept
1086
+ DM, follow-up, second-touch, cadence, branch, or other sequence-shaped copy, do
1087
+ not summarize it as ready; route back to message-generation and require a
1088
+ single first outbound send.
1056
1089
 
1057
1090
  After the main-chat message approval, update_campaign_brief writes `## Approved
1058
1091
  Message Template` with `{{...}}` tokens before any Generate Message cascade is
@@ -233,22 +233,26 @@ supplied and multiple source angles are viable, scout Signals, Sales Nav, and
233
233
  relevant domain/contact paths as independent branches when the host can do so.
234
234
  Call `get_source_scout_registry` first and use the returned canonical `name`
235
235
  values; the names below are the current registry entries. In Codex, explicitly
236
- spawn named custom scouts in the same turn:
236
+ spawn named custom scouts in the same turn when the current runtime exposes
237
+ them:
237
238
  `source-scout-linkedin-engagement` (LinkedIn Engagement Scout, backed by the
238
239
  `signal-discovery` provider prompt), `source-scout-sales-nav` (Sales Nav Scout), and
239
240
  `source-scout-prospeo-contact` (Prospeo Contact Scout) for the credible lanes. Codex
240
241
  does not infer subagent fan-out from generic "compare paths" wording. In Claude
241
242
  Code, invoke the generated `source-scout-linkedin-engagement`, `source-scout-sales-nav`,
242
- and/or `source-scout-prospeo-contact` Task/Agent subagents in one assistant message so
243
- Claude can run the source lanes concurrently/background. These agents come from
244
- the same canonical Sellable registry and must load their matching provider
245
- prompt before searching. The parent thread should not fetch every provider
246
- prompt first; that burns time and often degenerates into parallel tool calls
247
- instead of true subagents. When asking the user to approve a brief with
248
- Sellable finding leads, use an explicit label like `Approve brief + use
249
- background source scouts` so the next turn has a user-visible request to spawn
250
- scouts. If the host runs them sequentially, keep the output numeric but do not
251
- claim the source scout was parallel.
243
+ and/or `source-scout-prospeo-contact` Task/Agent subagents in one assistant
244
+ message only when the session lists those names, so Claude can run the source
245
+ lanes concurrently/background. These agents come from the same canonical
246
+ Sellable registry and must load their matching provider prompt before
247
+ searching. The parent thread should not fetch every provider prompt first; that
248
+ burns time and often degenerates into parallel tool calls instead of true
249
+ subagents. When asking the user to approve a brief with Sellable finding leads,
250
+ use an explicit label like `Approve brief + use background source scouts` only
251
+ when the current runtime exposes the named agents; otherwise use `Approve brief
252
+
253
+ - compare source paths`. If the host runs them sequentially or the named agents
254
+ are absent, keep the output numeric but do not claim the source scout was
255
+ parallel or surface install status to the customer.
252
256
 
253
257
  For post-lead work, call `get_post_find_leads_scout_registry` after source
254
258
  approval and use the returned canonical `name` values. Launch both returned
@@ -947,11 +947,11 @@
947
947
  "ownership": "proof inventory, token strategy, angle drafting, skeptical-prospect review, and selected winner only"
948
948
  }
949
949
  ],
950
- "earlyMessageStartRule": "After auto-execute-leads confirms the 10-row review batch and workflowTableId is ready, launch message-generation from brief.md, lead-review.md, lead-sample.json, and the imported campaign table sample. Legacy/resume preview prep still requires at least 5 probable good-fit rows. It may run beside filter-leads, but the cascade cannot queue until save_rubrics and update_campaign_brief both succeed.",
950
+ "earlyMessageStartRule": "After auto-execute-leads confirms the 10-row review batch and workflowTableId is ready, launch message-generation from brief.md, lead-review.md, lead-sample.json, and the imported campaign table sample. The message worker carries the campaign-launch message rules in its agent prompt; parent-thread fallback loads references/message-review-safety-gate.md. Legacy/resume preview prep still requires at least 5 probable good-fit rows. It may run beside filter-leads, but the cascade cannot queue until save_rubrics and update_campaign_brief both succeed.",
951
951
  "finalMessageReconcileRule": "message-validation.md may start before lead-filter.md exists, but before message-review it must cite only imported review-batch rows that still pass lead-filter.md. If the selected winner depends on a row later excluded by lead-filter.md, revise message-generation before message review.",
952
- "claudeRule": "In Claude Code, launch both returned post-find-leads scouts with Task/Agent subagents in the same assistant message when Task is available. Do not run filter first and then message generation unless subagents/background work are unavailable.",
953
- "codexRule": "In Codex, launch both returned post-find-leads scout names as disjoint subagents in the same assistant turn when the host exposes subagents for this run. If the host cannot spawn them, run the same branches sequentially and say so.",
954
- "fallback": "If real parallel branches are unavailable, run filter-leads and then message-generation in the parent thread. Do not claim background or parallel work in that fallback."
952
+ "claudeRule": "In Claude Code, launch both returned post-find-leads scouts with Task/Agent subagents in the same assistant message only when the current session lists those agent names. Do not run filter first and then message generation unless subagents/background work are unavailable.",
953
+ "codexRule": "In Codex, launch both returned post-find-leads scout names as disjoint subagents in the same assistant turn only when the host exposes those custom agents for this run. If the host cannot spawn them, run the same branches sequentially and say so.",
954
+ "fallback": "If real parallel branches are unavailable or the named agents are absent, run filter-leads and then message-generation in the parent thread with product MCP tools/assets. Do not customer-surface agent install status, and do not claim background or parallel work in that fallback."
955
955
  },
956
956
  {
957
957
  "action": "wait_for_post_lead_artifacts",
@@ -1014,7 +1014,7 @@
1014
1014
  "The post-lead workstreams are disjoint: filter-leads owns lead-filter.md/rubric.json; message-generation owns message-validation.md/message-prep.md/message-candidate-drafts.md.",
1015
1015
  "message-generation can start before lead-filter.md, but message-review cannot start until both lead-filter.md and message-validation.md exist and reconcile against the same lead-sample.json.",
1016
1016
  "Do not let filter-leads create a new message sample. Do not let message-generation fetch new prospects.",
1017
- "Before writing message-validation.md, message-generation must read 100% of the real generate-messages prompt via chunked get_subskill_prompt({ subskillName: \"generate-messages\", offset, limit }) calls.",
1017
+ "Before writing message-validation.md, message-generation must either run in the post-find-leads-message-scout agent that carries the campaign-launch message rules in its prompt or load get_subskill_asset({ subskillName: \"create-campaign-v2\", assetPath: \"references/message-review-safety-gate.md\" }) in the parent-thread fallback. Do not load the full long generate-messages prompt in create-campaign-v2; if the safety gate cannot safely approve the draft, route to revise-messaging with the concrete failure.",
1018
1018
  "Do not queue, enrich, attach sequence, or start until save_rubrics succeeds and the approved message set is synced into campaignBrief."
1019
1019
  ],
1020
1020
  "doNotAllow": [
@@ -1181,11 +1181,11 @@
1181
1181
  "onEnter": [
1182
1182
  {
1183
1183
  "action": "run_or_reconcile_subskill",
1184
- "target": "generate-messages",
1184
+ "target": "message-review-safety-gate",
1185
1185
  "mode": "DRY MODE",
1186
1186
  "sampleSource": "lead-sample.json from find-leads",
1187
1187
  "toolCallRequiredBeforeArtifacts": [
1188
- "get_subskill_prompt({ subskillName: \"generate-messages\", offset, limit }) until hasMore=false"
1188
+ "get_subskill_asset({ subskillName: \"create-campaign-v2\", assetPath: \"references/message-review-safety-gate.md\" }) unless the post-find-leads-message-scout agent prompt already supplied these rules"
1189
1189
  ],
1190
1190
  "skipIfFreshArtifactExists": "message-validation.md",
1191
1191
  "reconcileWith": "lead-filter.md; lead-sample.json remains the sample source"
@@ -1204,18 +1204,17 @@
1204
1204
  "message-validation.md"
1205
1205
  ],
1206
1206
  "allowedTools": [
1207
- "get_subskill_prompt",
1208
1207
  "get_subskill_asset",
1209
1208
  "AskUserQuestion",
1210
1209
  "request_user_input"
1211
1210
  ],
1212
1211
  "toolRules": [
1213
- "Before writing message-validation.md, message-review.md, or a message-review AskUserQuestion, the current run must read 100% of the real generate-messages prompt via chunked get_subskill_prompt({ subskillName: \"generate-messages\", offset, limit }) calls.",
1212
+ "Before writing message-validation.md, message-review.md, or a message-review AskUserQuestion, the current run must either execute inside the post-find-leads-message-scout agent that carries the campaign-launch message rules in its prompt or load get_subskill_asset({ subskillName: \"create-campaign-v2\", assetPath: \"references/message-review-safety-gate.md\" }) in the parent-thread fallback.",
1214
1213
  "Lead Sample Basis must cite rows from lead-sample.json produced by find-leads. lead-filter.md only gates those rows; it is not a source for a new sample.",
1215
1214
  "Do not hand-write message-validation.md from message-prep.md, message-candidate-drafts.md, or general campaign knowledge.",
1216
- "message-validation.md must prove the full generate-messages workflow ran: Gold Standard Strategy Map, Proof Inventory, Token Fill Rules, Token Adherence Table, Angle Drafts, Kill / Combine Review, Finalists, Finalizer Pass, Gold-Standard Quality Gate, Skeptical Prospect Review, Winner Gate, and a raw sendable Selected Winner are required before message-review can recommend approve-message.",
1217
- "If the Codex-hosted output is plausible but weaker than the loaded gold-standard examples, stop at message-review with revise-messaging. Do not continue to approval or mint just because the mechanical flow worked.",
1218
- "If message review is ready but the generate-messages prompt was not retrieved in this run, route back to message-generation instead of asking the user to approve the message.",
1215
+ "message-validation.md must prove the message-review safety-gate workflow ran: Gold Standard Strategy Map, Campaign Element Pool, Current Campaign Translation, Token Fill Rules, Token Adherence Table, Angle Drafts, Kill / Combine Review, Finalizer Pass, Gold-Standard Quality Gate, Skeptical Prospect Review, Winner Gate, and a raw sendable Selected Winner are required before message-review can recommend approve-message.",
1216
+ "If the hosted output is plausible but weaker than the safety-gate gold-standard rules, stop at message-review with revise-messaging. Do not continue to approval or mint just because the mechanical flow worked.",
1217
+ "Do not load the full generate-messages subskill in create-campaign-v2. If the safety gate is missing a needed campaign-specific rule or the first safety-gate draft fails quality gates, route to revise-messaging with the concrete failure instead of loading the long prompt.",
1219
1218
  "The approved message set must be written back into campaignBrief before validate-sample queues the review-batch cascade."
1220
1219
  ],
1221
1220
  "doNotAllow": [
@@ -1867,6 +1866,19 @@
1867
1866
  "doNotRetain": "rows_payload",
1868
1867
  "contextBloatNote": "Pass includeRows=false so the MCP tool returns stats only; 25 rows ≈ 67KB and 100 rows ≈ 268KB when row payloads are retained (see references/sample-validation-loop.md §Known Tool Behaviors #2)."
1869
1868
  },
1869
+ {
1870
+ "action": "handle_partial_or_timeout_sample",
1871
+ "when": "wait_for_rubric_results.ready_false_or_reason_timeout",
1872
+ "rule": "Do not repeat waits indefinitely. If active processing is visible, wait once more at most. Otherwise treat returned passRate/stats as a partial sample and stop before Settings with sample_revision_required when passed rows are below minProjectedPass or generated messages are incomplete.",
1873
+ "customerStatus": "sample-needs-revision",
1874
+ "showFields": [
1875
+ "passRate.completed",
1876
+ "passRate.passed",
1877
+ "passRate.pending",
1878
+ "passRate.percent",
1879
+ "stats.messagesCount"
1880
+ ]
1881
+ },
1870
1882
  {
1871
1883
  "action": "compute_projected_pass",
1872
1884
  "formula": "round(passInSample / sampleSize * importLimit)",
@@ -1906,6 +1918,8 @@
1906
1918
  "revision_round_persists_across_resume",
1907
1919
  "wait_for_rubric_results_never_retain_rows_payload_in_tail_context",
1908
1920
  "wait_for_rubric_results_targetCount_always_explicit",
1921
+ "timeout_never_repeats_without_customer_handoff",
1922
+ "timeout_or_underfloor_sample_never_advances_to_settings",
1909
1923
  "review_batch_cascade_waits_for_saved_rubrics_and_approved_template"
1910
1924
  ],
1911
1925
  "watchRequired": true,
@@ -1915,6 +1929,9 @@
1915
1929
  ],
1916
1930
  "transitions": {
1917
1931
  "sample_validated": "auto-execute-messaging",
1932
+ "sample_revision_required": "lead-review",
1933
+ "revise_leads": "find-leads",
1934
+ "revise_rubric": "filter-rubric",
1918
1935
  "escalation_triggered": "escalation"
1919
1936
  },
1920
1937
  "requiredCampaignState": [
@@ -32,12 +32,14 @@ Before showing the commit gate, the draft directory must contain all of:
32
32
  - `message-validation.md`
33
33
  - `approval-packet.md`
34
34
 
35
- The current run must also have retrieved the real message-generation prompt
36
- through `get_message_prompt({ promptName: "generate-messages" })` or
37
- `get_subskill_prompt({ subskillName: "generate-messages" })` before
38
- `message-validation.md`, `approval-packet.md`, or the commit gate is written.
39
- If the message template was written directly from memory or planning artifacts,
40
- do not show the commit gate; route back to message generation.
35
+ The current run must also have retrieved message-generation rules before
36
+ `message-validation.md`, `approval-packet.md`, or the commit gate is written. In
37
+ `create-campaign-v2`, use the compact
38
+ `references/message-review-safety-gate.md` asset or the embedded
39
+ `post-find-leads-message-scout` prompt. Do not load the full long-form
40
+ `generate-messages` subskill for the hosted campaign launch path. If the message
41
+ template was written directly from memory or planning artifacts, do not show the
42
+ commit gate; route back to message generation.
41
43
 
42
44
  `rubric.json` is optional but strongly preferred — when missing, derive it from
43
45
  `lead-filter.md` in the atomic mint step.
@@ -0,0 +1,96 @@
1
+ # Message Review Safety Gate
2
+
3
+ Use this reference for `create-campaign-v2` message review only in two cases:
4
+ when these rules are embedded inside the `post-find-leads-message-scout` agent
5
+ prompt, or when the host cannot launch that agent and the parent thread needs a
6
+ compact fallback. It is the campaign-launch subset of `generate-messages`: enough
7
+ to prove a truthful first-send message, rendered token examples, and an approval
8
+ decision without loading the full long-form message-generation prompt into the
9
+ main Claude/Codex thread.
10
+
11
+ In a normal installed Claude/Codex session, the message scout owns these rules.
12
+ The parent-thread asset is a compatibility fallback, not a shortcut. Do not load
13
+ the full `generate-messages` subskill in this flow. If the safety gate is missing
14
+ a needed campaign-specific rule or the draft fails quality gates, stop at
15
+ `revise-messaging` with the exact missing rule/failure instead of pulling the
16
+ long prompt into the main thread.
17
+
18
+ ## Required Workflow
19
+
20
+ 1. Read `brief.md`, `lead-review.md`, `lead-sample.json`, and `lead-filter.md`
21
+ when present.
22
+ 2. Pick 2-3 sample rows from `lead-sample.json`, preferring rows that pass the
23
+ filter or are likely to pass. Do not invent new rows.
24
+ 3. Build the message from the approved brief and the sampled rows. Use only
25
+ proof that appears in the brief, source review, sample, or explicit user
26
+ answers. Unsupported reply-rate, meeting-rate, ROI, revenue, and customer-logo
27
+ claims are blocked.
28
+ 4. Produce `message-validation.md` with the required sections below.
29
+ 5. Produce `message-review.md` as the customer-facing checkpoint.
30
+ 6. Ask exactly `approve-message` or `revise-messaging`. Do not import, queue,
31
+ attach sequence, or start before `approve-message`.
32
+
33
+ ## Required `message-validation.md` Sections
34
+
35
+ - `Status`
36
+ - `Mode`
37
+ - `Lead Sample Basis`
38
+ - `Strongest Reply Reason`
39
+ - `Campaign Element Pool`
40
+ - `Gold Standard Strategy Map`
41
+ - `Current Campaign Translation`
42
+ - `Token Fill Rules`
43
+ - `Token Adherence Table`
44
+ - `Angle Drafts`
45
+ - `Kill / Combine Review`
46
+ - `Finalizer Pass`
47
+ - `Gold-Standard Quality Gate`
48
+ - `Skeptical Prospect Review`
49
+ - `Winner Gate`
50
+ - `Selected Winner`
51
+ - `Findings`
52
+ - `Recommendation`
53
+
54
+ Keep this artifact concise. It should prove the reasoning path, not reproduce
55
+ the full framework.
56
+
57
+ ## Message Quality Gates
58
+
59
+ - The selected winner must be one first outbound send only. No post-accept DM,
60
+ follow-up, cadence branch, or sequence copy.
61
+ - The message must explain what the product is and what it does in plain
62
+ language before asking for a call.
63
+ - The message must use a truthful buyer-side reply reason, not just sender
64
+ biography.
65
+ - If using a template, include at least one supported `{{token}}` and show a
66
+ complete rendered good-fill example and complete rendered omit/fallback
67
+ example.
68
+ - Do not use internal tokens such as `{{profile_signal}}` in customer-facing
69
+ copy.
70
+ - Do not put bracketed instructions in the message body, such as `[ROW_BRIDGE]`,
71
+ `[insert]`, `[generated]`, or any instruction for a later model to fill.
72
+ - Optional row-specific personalization must be grounded in a row field or
73
+ omitted entirely. Never use generic filler like "your work" or "your team."
74
+ - Subject lines should be short, buyer-relevant, and specific. Avoid
75
+ `quick question`, `demo`, `founder call`, sender names, and generic `outbound`.
76
+ - If the message is plausible but not ready to send, set
77
+ `Recommendation: revise-messaging`.
78
+
79
+ ## Customer-Facing `message-review.md`
80
+
81
+ Render this in chat before asking:
82
+
83
+ - `Status: message-review`
84
+ - `Subject:`
85
+ - `Message:` with the tokenized template or approved AI-native message
86
+ - `Rendered examples:`
87
+ - `Good token fill:`
88
+ - `Good omit:`
89
+ - `Token notes:`
90
+ - `My take:`
91
+ - `Suggested adjustment:`
92
+ - `Question: approve-message or revise-messaging?`
93
+ - `Recommendation: approve-message` or `Recommendation: revise-messaging`
94
+
95
+ `My take` and `Suggested adjustment` must be specific. The adjustment can be
96
+ "approve as-is, or revise once to X if you want Y"; it must not be empty.
@@ -58,18 +58,37 @@ auto-revise leads.
58
58
 
59
59
  7. call `wait_for_rubric_results` with `includeRows=false`; extract ONLY:
60
60
  - ready: boolean
61
+ - reason: string when `ready=false`
61
62
  - passRate.completed: number
63
+ - passRate.passed: number
64
+ - partialResult: object when present
62
65
  - stats: object
63
66
  Never retain the full rows payload in tail context.
64
67
  - 25 rows ≈ 67KB of payload (full post bodies in carryData)
65
68
  - 100 rows ≈ 268KB; 500 rows ≈ 1.3MB — blows context on Opus
66
69
  (see §Known Tool Behaviors #2)
67
70
 
68
- 8. compute metrics:
71
+ 8. if `ready=false` and `reason="timeout"`:
72
+ - do NOT keep polling indefinitely
73
+ - treat the returned passRate/stats as a partial sample
74
+ - if active processing is visible (`processingCount > 0` or a similar
75
+ running-cell stat), one additional wait is allowed
76
+ - otherwise diagnose the partial sample now
77
+ - if passed rows are below `sample.minProjectedPass` or generated messages
78
+ are missing for passing rows, stop before Settings with:
79
+ `Status: sample-needs-revision`
80
+ - show the concrete numbers: completed, passed, pending, pass percent, and
81
+ message count when available
82
+ - offer the next product choices: revise source, revise filter/rubric, or
83
+ wait once only if there is active processing
84
+ - never advance to Settings or awaiting-user-greenlight on a timeout or
85
+ underfloor partial sample
86
+
87
+ 9. compute metrics:
69
88
  passInSample = count of review-batch rows where rubric passed
70
89
  projectedPass = round(passInSample / sampleSize * importLimit)
71
90
 
72
- 9. branch:
91
+ 10. branch:
73
92
  if projectedPass >= minProjectedPass:
74
93
  proceed to Step 15 (auto-execute-messaging) with this review batch
75
94
  else:
@@ -140,6 +159,19 @@ Workaround: strip `carryData` from each row IMMEDIATELY after reading
140
159
  and BEFORE retaining. Prefer a minimal projection (name, title,
141
160
  company, enrichCellId, enrichStatus) over the default shape.
142
161
 
162
+ ### 5. `wait_for_rubric_results` can timeout with enough signal to decide
163
+
164
+ Observed: a 10-row review batch may return `ready=false`, `reason="timeout"`,
165
+ and partial stats such as 9/10 scored, 2 passing, 2 messages generated. That is
166
+ enough to diagnose an underperforming sample. Waiting again without active
167
+ processing makes the experience feel frozen.
168
+
169
+ Workaround: treat timeout stats as a partial sample. If the pass count is below
170
+ the configured floor or message generation has not covered the passing rows,
171
+ stop at `Status: sample-needs-revision` before Settings. Show the completed /
172
+ passed / pending counts and ask whether to revise source, revise filter/rubric,
173
+ or wait once only if active processing is still visible.
174
+
143
175
  ## Projected Pass Math
144
176
 
145
177
  ```text
@@ -269,10 +269,19 @@ Shape:
269
269
  queue_cells({ tableId: workflowTableId, cellIds: reviewBatchEnrichCellIds })
270
270
  wait_for_campaign_table_ready({ tableId: workflowTableId })
271
271
  get_rows_minimal({ tableId: workflowTableId })
272
+ wait_for_rubric_results({ tableId: workflowTableId, targetCount: cohortSize, includeRows: false })
272
273
  passInSample = count of first sampleSize review-batch rows with passesRubric === true
273
274
  projectedPass = round(passInSample / sampleSize * importLimit)
274
275
 
275
- if projectedPass >= minProjectedPass:
276
+ if wait_for_rubric_results.ready === false and reason === "timeout":
277
+ use the partial passRate/stats as the sample diagnostic
278
+ if active processing is visible:
279
+ wait one more time at most
280
+ else:
281
+ stop before Settings with Status: sample-needs-revision
282
+ show completed, passed, pending, pass percent, and message count when available
283
+ ask whether to revise source, revise filter/rubric, or wait once only if still processing
284
+ else if projectedPass >= minProjectedPass:
276
285
  advance to Step 15
277
286
  else:
278
287
  diagnose brief-vs-list per sample-validation-loop.md
@@ -293,6 +302,11 @@ Do NOT reset to 0 on resume — a stale resume must still respect the
293
302
  A "list problem" diagnosis NEVER triggers autonomous
294
303
  `update_campaign_brief`. Escalate instead.
295
304
 
305
+ A timeout or underfloor partial sample NEVER advances to Settings,
306
+ awaiting-user-greenlight, or start-campaign. It must hand off as
307
+ `sample_revision_required` with the observed counts so the user can decide
308
+ whether the source or filter should change.
309
+
296
310
  When the sample passes the projected-pass floor, call
297
311
  `update_campaign({ campaignId, currentStep: "auto-execute-messaging" })`
298
312
  and orient the user that messaging will complete for the review batch only.
@@ -68,9 +68,10 @@ The kickoff doc is the resume surface. Re-open it before repeating discovery wor
68
68
  - Before source-scout dispatch, call `get_source_scout_registry` and use the
69
69
  returned `name` values. The current canonical names are listed below for
70
70
  readability, but the registry is the source of truth for install/runtime.
71
- - If Codex subagents are available, run the scout lanes with named custom
72
- agents. Spawn one agent per credible lane in the same turn, then wait for all
73
- lane results before synthesizing `lead-review.md`:
71
+ - If Codex subagents are available and the current runtime exposes the returned
72
+ names, run the scout lanes with named custom agents. Spawn one agent per
73
+ credible lane in the same turn, then wait for all lane results before
74
+ synthesizing `lead-review.md`:
74
75
  - `source-scout-linkedin-engagement` (display: LinkedIn Engagement Scout) for
75
76
  active LinkedIn posts and engagers; internally this uses the
76
77
  `signal-discovery` provider prompt plus `search_signals` /
@@ -81,15 +82,20 @@ The kickoff doc is the resume surface. Re-open it before repeating discovery wor
81
82
  account/domain and verified-contact expansion.
82
83
  - Common parallel comparisons are LinkedIn Engagement + Sales Nav, LinkedIn
83
84
  Engagement + Prospeo Contact, or all three when each lane is credible.
84
- - If Claude `Task`/`Agent` is available, deep exploration uses the file-backed
85
- Claude explorer agents installed from the same canonical Sellable agent
86
- registry. Launch every credible lane in the same assistant message so Claude
87
- Code can run them concurrently/background:
85
+ - If Claude `Task`/`Agent` is available and the current Claude session lists
86
+ the returned source-scout names, deep exploration uses the file-backed Claude
87
+ explorer agents installed from the same canonical Sellable agent registry.
88
+ Launch every credible lane in the same assistant message so Claude Code can
89
+ run them concurrently/background:
88
90
  - `./.claude/agents/source-scout-linkedin-engagement.md`
89
91
  - `./.claude/agents/source-scout-sales-nav.md`
90
92
  - `./.claude/agents/source-scout-prospeo-contact.md`
91
93
  - These Claude agents must have explicit Sellable MCP tool allowlists and must
92
- load the matching provider prompt before searching.
94
+ load the matching provider prompt before searching. They are optional
95
+ acceleration: if the names are absent, do not mention the missing install or
96
+ agent availability in customer-facing output. Run the same provider probes
97
+ from the parent thread with MCP tools and keep `lead-review.md` focused on
98
+ source evidence.
93
99
  - The parent thread should not preload every provider prompt before launching
94
100
  scouts. Spawn the credible scouts first; each scout loads only the provider
95
101
  prompt for its lane and returns structured evidence.
@@ -261,9 +267,10 @@ If the first probe is weak or noisy and enough context exists, try 1-2 alternate
261
267
  If the first probe has good quality but insufficient scale, iterate 1-2 times to widen intelligently before returning.
262
268
  When two or more source angles are viable, run the provider probes in real
263
269
  parallel when the host/runtime allows it. For Codex, explicitly spawn the
264
- named source scout agents above; Codex will not infer subagent fan-out from
265
- general "compare sources" wording. For Claude Code, explicitly invoke the
266
- matching `source-scout-*` Task/Agent subagents in one message instead of
270
+ named source scout agents above only when those returned names are visible;
271
+ Codex will not infer subagent fan-out from general "compare sources" wording.
272
+ For Claude Code, explicitly invoke the matching `source-scout-*` Task/Agent
273
+ subagents in one message only when the current session exposes them, instead of
267
274
  probing one lane, waiting, then probing the next. Examples: LinkedIn Engagement
268
275
 
269
276
  - Sales Nav, LinkedIn Engagement + Prospeo Contact, or Sales Nav + Prospeo
@@ -433,8 +440,10 @@ Show the light-touch results and ask whether to deep-dispatch or iterate.
433
440
  ## Layer 3: Selective deep exploration
434
441
 
435
442
  Spawn only the validated paths from Gate 1. In Claude Code, invoke all selected
436
- `source-scout-*` agents in one turn so source scouting is concurrent when the
437
- runtime supports it.
443
+ `source-scout-*` agents in one turn only when the current session exposes those
444
+ agent names, so source scouting is concurrent when the runtime supports it. If
445
+ they are absent, run the validated provider probes from the parent thread
446
+ without telling the customer about host-agent install state.
438
447
 
439
448
  - `source-scout-linkedin-engagement`
440
449
  - `source-scout-sales-nav`