@sellable/install 0.1.310 → 0.1.312

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
@@ -119,30 +119,9 @@ It creates:
119
119
  /srv/hermes/profiles/acme/sellable/config.json
120
120
  /srv/hermes/profiles/acme/sellable/configs/
121
121
  /srv/hermes/profiles/acme/skills/sellable/
122
- /srv/hermes/bin/entrypoint-acme.sh
123
122
  /srv/hermes/profiles/acme/.env # only when Slack token inputs are supplied
124
123
  ```
125
124
 
126
- For profile paths under a gateway root such as
127
- `/srv/hermes/profiles/acme`, bootstrap also registers
128
- `/srv/hermes/profiles/acme/skills/sellable` in the gateway root
129
- `/srv/hermes/config.yaml` as `skills.external_dirs`. Keep this behavior:
130
- Hermes Desktop command autocomplete scans the gateway/root skill config, not
131
- only the selected profile's local skill tree.
132
-
133
- Bootstrap also writes `/srv/hermes/bin/entrypoint-acme.sh`. Point the Hermes
134
- Docker service at that script when this profile should be the Desktop/dashboard
135
- default:
136
-
137
- ```yaml
138
- entrypoint: ["/opt/data/bin/entrypoint-acme.sh"]
139
- ```
140
-
141
- The wrapper starts `hermes gateway run` when needed, then intentionally runs
142
- `hermes -p acme dashboard --isolated`. Without `--isolated`, Hermes OS chat can
143
- see `/sellable-*` commands while Hermes Desktop autocomplete misses them because
144
- the dashboard backend is not scoped to the selected profile.
145
-
146
125
  If no token is supplied, bootstrap still creates a pending
147
126
  `sellable/config.json` so the profile path is concrete from the first MCP
148
127
  launch. Finish auth with:
@@ -13,7 +13,7 @@ import {
13
13
  writeFileSync,
14
14
  } from "node:fs";
15
15
  import { homedir } from "node:os";
16
- import { basename, dirname, join, relative, resolve } from "node:path";
16
+ import { dirname, join, relative, resolve } from "node:path";
17
17
  import { stdout as output } from "node:process";
18
18
  import { createInterface } from "node:readline/promises";
19
19
  import { fileURLToPath } from "node:url";
@@ -1180,10 +1180,9 @@ const REFRESH_SENDER_ENGAGEMENT_ALLOWED_TOOLS = [
1180
1180
  "mcp__sellable__get_campaign_table_schema",
1181
1181
  "mcp__sellable__list_tables",
1182
1182
  "mcp__sellable__get_rows_minimal",
1183
+ "mcp__sellable__refresh_sender_engagement",
1183
1184
  "mcp__sellable__fetch_linkedin_posts",
1184
- "mcp__sellable__select_promising_posts",
1185
1185
  "mcp__sellable__fetch_post_engagers",
1186
- "mcp__sellable__add_on_demand_leads",
1187
1186
  ];
1188
1187
 
1189
1188
  function allowedToolsYaml(tools) {
@@ -2145,10 +2144,10 @@ ${allowedToolsYaml(REFRESH_SENDER_ENGAGEMENT_ALLOWED_TOOLS)}
2145
2144
 
2146
2145
  Use this as the customer-facing entrypoint for the Sellable
2147
2146
  \`refresh-sender-engagement\` workflow. It finds active sender-owned Post
2148
- Engagers / Signal Discovery campaigns, fetches recent sender-authored LinkedIn
2149
- posts, pulls latest engagers, filters to ICP, and adds net-new leads with
2150
- dedupe. It never creates messages, approves rows, schedules sends, or launches
2151
- campaigns.
2147
+ Engagers campaigns, runs the typed \`refresh_sender_engagement\` command in
2148
+ \`mode:"dry_run"\`, and gates any \`mode:"apply"\` call with the returned
2149
+ \`dryRunFingerprint\`. It never creates messages, approves rows, schedules
2150
+ sends, or launches campaigns.
2152
2151
 
2153
2152
  ## Bootstrap
2154
2153
 
@@ -2168,9 +2167,15 @@ Desktop, then start a new thread.
2168
2167
  \`mcp__sellable__get_subskill_prompt({ subskillName: "refresh-sender-engagement" })\`.
2169
2168
  If the response has \`hasMore=true\`, continue with \`nextOffset\` until
2170
2169
  \`hasMore=false\`.
2171
- 3. Follow the canonical prompt exactly. Target active/paused sender-owned Post
2172
- Engagers campaigns backed by Signal Discovery, and do not touch shared lanes
2173
- or send-state tools.
2170
+ 3. Follow the canonical prompt exactly. Prefer
2171
+ \`mcp__sellable__refresh_sender_engagement\` for dry-run and apply. Do not use
2172
+ primitive lead import tools as an alternate write path.
2173
+ 4. For manual runs, show the dry-run target workspace, sender, campaign/table,
2174
+ expected impact, and \`dryRunFingerprint\`; wait for explicit approval before
2175
+ apply. For scheduled automations, apply only with the fingerprint from the
2176
+ immediately preceding dry run.
2177
+ 5. Do not touch shared lanes, campaign status, approval, message preparation, or
2178
+ send-state tools.
2174
2179
 
2175
2180
  ## MCP Prompt Fallback
2176
2181
 
@@ -3663,87 +3668,6 @@ function hermesSkillsRoot(opts = {}) {
3663
3668
  return join(hermesHome(opts), "skills", "sellable");
3664
3669
  }
3665
3670
 
3666
- function hermesGatewayRoot(opts = {}) {
3667
- const profileRoot = hermesHome(opts);
3668
- const profilesRoot = dirname(profileRoot);
3669
- if (basename(profilesRoot) !== "profiles") return null;
3670
- return dirname(profilesRoot);
3671
- }
3672
-
3673
- function hermesGatewayConfigPath(opts = {}) {
3674
- const root = hermesGatewayRoot(opts);
3675
- if (!root) return null;
3676
- return join(root, "config.yaml");
3677
- }
3678
-
3679
- function hermesDashboardEntrypointPath(opts = {}, profile = "") {
3680
- const root = hermesGatewayRoot(opts);
3681
- if (!root) return null;
3682
- const safeProfile = String(profile || basenameSafe(hermesHome(opts))).replace(
3683
- /[^A-Za-z0-9_.-]/g,
3684
- "_"
3685
- );
3686
- return join(root, "bin", `entrypoint-${safeProfile}.sh`);
3687
- }
3688
-
3689
- function hermesDashboardCommand(profile) {
3690
- return `hermes -p ${profile} dashboard --isolated --host 0.0.0.0 --port 4860 --no-open --skip-build`;
3691
- }
3692
-
3693
- function hermesDashboardEntrypointContent(opts, profile) {
3694
- const quotedProfile = shellQuote(profile);
3695
- const quotedProfileRoot = shellQuote(hermesHome(opts));
3696
- const gatewayRoot = hermesGatewayRoot(opts);
3697
- const quotedGatewayRoot = shellQuote(gatewayRoot || dirname(hermesHome(opts)));
3698
- const quotedDashboardLog = shellQuote(join(hermesHome(opts), "logs", "dashboard.log"));
3699
- return `#!/usr/bin/env bash
3700
- set -euo pipefail
3701
-
3702
- # This file is generated by sellable hermes profile bootstrap.
3703
- # It preserves the Hostinger image startup shape, but launches the dashboard in
3704
- # an isolated Sellable profile. Without --isolated, Hermes OS chat can see
3705
- # profile skills while Desktop slash autocomplete stays on the default profile.
3706
- profile=${quotedProfile}
3707
- profile_root=${quotedProfileRoot}
3708
- gateway_root=${quotedGatewayRoot}
3709
- dashboard_log=${quotedDashboardLog}
3710
- dashboard_port="\${HERMES_DASHBOARD_PORT:-4860}"
3711
- run_as=()
3712
- if command -v gosu >/dev/null 2>&1; then
3713
- run_as=(gosu hermes)
3714
- fi
3715
-
3716
- chown -R hermes:hermes "$gateway_root" 2>/dev/null || true
3717
- mkdir -p "$gateway_root/logs" "$profile_root/logs"
3718
- chown -R hermes:hermes "$gateway_root/logs" "$profile_root/logs" 2>/dev/null || true
3719
-
3720
- if [[ -f "$gateway_root/config.yaml" ]]; then
3721
- if ! pgrep -f "hermes gateway run" >/dev/null 2>&1; then
3722
- "\${run_as[@]}" nohup hermes gateway run >>"$gateway_root/logs/gateway.log" 2>&1 </dev/null &
3723
- fi
3724
- fi
3725
-
3726
- for v in ADMIN_USERNAME ADMIN_PASSWORD; do
3727
- if [[ -z "\${!v:-}" ]]; then
3728
- echo "missing required environment variable $v" >&2
3729
- exit 1
3730
- fi
3731
- done
3732
-
3733
- export HERMES_DASHBOARD_BASIC_AUTH_USERNAME="$ADMIN_USERNAME"
3734
- export HERMES_DASHBOARD_BASIC_AUTH_PASSWORD="$ADMIN_PASSWORD"
3735
-
3736
- exec "\${run_as[@]}" hermes -p "$profile" dashboard --isolated --host 0.0.0.0 --port "$dashboard_port" --no-open --skip-build >>"$dashboard_log" 2>&1
3737
- `;
3738
- }
3739
-
3740
- function writeHermesDashboardEntrypoint(opts, profile) {
3741
- const path = hermesDashboardEntrypointPath(opts, profile);
3742
- if (!path) return null;
3743
- writeFile(path, hermesDashboardEntrypointContent(opts, profile), opts, 0o755);
3744
- return path;
3745
- }
3746
-
3747
3671
  function hermesLikelyInstalled() {
3748
3672
  return (
3749
3673
  Boolean(process.env.HERMES_HOME?.trim()) ||
@@ -3823,38 +3747,6 @@ function writeHermesMcpServer(opts) {
3823
3747
  return configPath;
3824
3748
  }
3825
3749
 
3826
- function writeHermesGatewaySkillDirectory(opts) {
3827
- const configPath = hermesGatewayConfigPath(opts);
3828
- if (!configPath || configPath === hermesConfigPath(opts)) return null;
3829
-
3830
- const skillDir = hermesSkillsRoot(opts);
3831
- const raw = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
3832
- const config = parseHermesConfig(raw, configPath);
3833
- // Hermes Desktop slash autocomplete scans the gateway/root HERMES_HOME. A
3834
- // profile can execute its own skills without this, but Desktop will not show
3835
- // /sellable-* commands until the root scanner sees the profile skill dir.
3836
- const skills =
3837
- config.skills && typeof config.skills === "object" && !Array.isArray(config.skills)
3838
- ? config.skills
3839
- : {};
3840
- const current = Array.isArray(skills.external_dirs)
3841
- ? skills.external_dirs
3842
- : skills.external_dirs
3843
- ? [skills.external_dirs]
3844
- : [];
3845
- const externalDirs = current.includes(skillDir) ? current : [...current, skillDir];
3846
- const nextConfig = {
3847
- ...config,
3848
- skills: {
3849
- ...skills,
3850
- external_dirs: externalDirs,
3851
- },
3852
- };
3853
-
3854
- writeHermesConfig(configPath, nextConfig, opts);
3855
- return { configPath, skillDir };
3856
- }
3857
-
3858
3750
  function installHermesSkills(opts) {
3859
3751
  const root = hermesSkillsRoot(opts);
3860
3752
  for (const skill of hermesSkillDefinitions()) {
@@ -3875,15 +3767,8 @@ function installHermes(opts) {
3875
3767
  try {
3876
3768
  skillsRoot = installHermesSkills(opts);
3877
3769
  const configPath = writeHermesMcpServer(opts);
3878
- const gateway = writeHermesGatewaySkillDirectory(opts);
3879
3770
  logVerbose(`${C.grey}+ Hermes config updated: ${configPath}${C.reset}`);
3880
- return {
3881
- installed: true,
3882
- configPath,
3883
- skillsRoot,
3884
- gatewayConfigPath: gateway?.configPath || null,
3885
- gatewayExternalSkillsDir: gateway?.skillDir || null,
3886
- };
3771
+ return { installed: true, configPath, skillsRoot };
3887
3772
  } catch (err) {
3888
3773
  if (skillsRoot && !opts.dryRun) {
3889
3774
  rmSync(skillsRoot, { recursive: true, force: true });
@@ -4121,8 +4006,6 @@ function runHermesProfileBootstrap(argv) {
4121
4006
  sellableConfigPath,
4122
4007
  sellableConfigsDir,
4123
4008
  skillsRoot: hermesSkillsRoot(opts),
4124
- gatewayConfigPath: hermesGatewayConfigPath(opts),
4125
- hermesDashboardEntrypointPath: hermesDashboardEntrypointPath(opts, profile),
4126
4009
  },
4127
4010
  auth: {
4128
4011
  apiUrl: opts.apiUrl,
@@ -4142,12 +4025,6 @@ function runHermesProfileBootstrap(argv) {
4142
4025
  .map(([key, value]) => [key, tokenFingerprint(value)])
4143
4026
  ),
4144
4027
  },
4145
- dashboard: {
4146
- command: hermesDashboardCommand(profile),
4147
- composeEntrypoint: hermesDashboardEntrypointPath(opts, profile)
4148
- ? `entrypoint: ["${hermesDashboardEntrypointPath(opts, profile)}"]`
4149
- : null,
4150
- },
4151
4028
  created: [],
4152
4029
  updated: [],
4153
4030
  preserved: [],
@@ -4213,12 +4090,6 @@ function runHermesProfileBootstrap(argv) {
4213
4090
  }
4214
4091
 
4215
4092
  const hermesConfigExisted = existsSync(hermesConfigPath(opts));
4216
- const gatewayConfigPath = hermesGatewayConfigPath(opts);
4217
- const gatewayConfigExisted = gatewayConfigPath ? existsSync(gatewayConfigPath) : false;
4218
- const dashboardEntrypointPath = hermesDashboardEntrypointPath(opts, profile);
4219
- const dashboardEntrypointExisted = dashboardEntrypointPath
4220
- ? existsSync(dashboardEntrypointPath)
4221
- : false;
4222
4093
  const skillsExisted = existsSync(hermesSkillsRoot(opts));
4223
4094
  const authExisted = existsSync(sellableConfigPath);
4224
4095
  const configsDirExisted = existsSync(sellableConfigsDir);
@@ -4257,7 +4128,6 @@ function runHermesProfileBootstrap(argv) {
4257
4128
  mkdirSync(sellableConfigsDir, { recursive: true, mode: 0o700 });
4258
4129
  writeHermesProfileSellableConfig(sellableConfigPath, nextAuth, opts);
4259
4130
  installHermes(opts);
4260
- writeHermesDashboardEntrypoint(opts, profile);
4261
4131
  if (Object.values(slackTokens).some(Boolean)) {
4262
4132
  for (const [key, value] of Object.entries(slackTokens)) {
4263
4133
  if (value) envState.entries.set(key, value);
@@ -4273,20 +4143,6 @@ function runHermesProfileBootstrap(argv) {
4273
4143
  (hermesConfigExisted ? summary.updated : summary.created).push(
4274
4144
  hermesConfigPath(opts)
4275
4145
  );
4276
- if (gatewayConfigPath) {
4277
- (gatewayConfigExisted ? summary.updated : summary.created).push(
4278
- gatewayConfigPath
4279
- );
4280
- }
4281
- if (dashboardEntrypointPath) {
4282
- (dashboardEntrypointExisted ? summary.updated : summary.created).push(
4283
- dashboardEntrypointPath
4284
- );
4285
- } else {
4286
- summary.skipped.push(
4287
- "Hermes dashboard entrypoint: profile root is not under a gateway profiles directory"
4288
- );
4289
- }
4290
4146
  (skillsExisted ? summary.updated : summary.created).push(hermesSkillsRoot(opts));
4291
4147
  if (Object.values(slackTokens).some(Boolean)) {
4292
4148
  (envExisted ? summary.updated : summary.created).push(envPath);
@@ -35,6 +35,7 @@ export const REQUIRED_SELLABLE_MCP_TOOLS = [
35
35
  "refresh_paid_inmail_credits",
36
36
  "run_scheduler_sweep",
37
37
  "refill_sends",
38
+ "refresh_sender_engagement",
38
39
  "setup_evergreen_campaigns",
39
40
  "start_campaign_message_preparation",
40
41
  "get_campaign_message_preparation_status",
@@ -75,6 +76,22 @@ export const CREATE_CAMPAIGN_SMOKE_CALLS = [
75
76
  limit: 1200,
76
77
  },
77
78
  },
79
+ {
80
+ name: "get_subskill_prompt",
81
+ arguments: {
82
+ subskillName: "refresh-sender-engagement",
83
+ offset: 0,
84
+ limit: 1200,
85
+ },
86
+ },
87
+ {
88
+ name: "get_subskill_prompt",
89
+ arguments: {
90
+ subskillName: "create-evergreen-campaigns",
91
+ offset: 0,
92
+ limit: 4000,
93
+ },
94
+ },
78
95
  {
79
96
  name: "refill_sends",
80
97
  arguments: {
@@ -221,10 +238,17 @@ function summarizeToolResult(name, result) {
221
238
  }
222
239
 
223
240
  if (name === "get_subskill_prompt" && parsed) {
241
+ const prompt = typeof parsed.prompt === "string" ? parsed.prompt : "";
224
242
  return {
225
243
  ...summary,
226
244
  subskillName: parsed.name || null,
227
- promptChars: typeof parsed.prompt === "string" ? parsed.prompt.length : 0,
245
+ promptChars: prompt.length,
246
+ connectionOnlyEvergreenGuidance:
247
+ parsed.name === "create-evergreen-campaigns"
248
+ ? prompt.includes('campaignSequenceOptions:{ mode:"connection_only" }') &&
249
+ prompt.includes('templateRef:"connection_only"') &&
250
+ prompt.includes('sequenceReceipt.actionTypes:["send_invite"]')
251
+ : null,
228
252
  hasMore: parsed.hasMore === true,
229
253
  nextOffset:
230
254
  typeof parsed.nextOffset === "number" ? parsed.nextOffset : null,
@@ -300,6 +324,21 @@ async function verifyCreateCampaignSmoke({
300
324
  completedAt: new Date().toISOString(),
301
325
  };
302
326
  }
327
+ if (
328
+ call.name === "get_subskill_prompt" &&
329
+ call.arguments?.subskillName === "create-evergreen-campaigns" &&
330
+ summary.connectionOnlyEvergreenGuidance !== true
331
+ ) {
332
+ return {
333
+ ok: false,
334
+ missingTools: [],
335
+ calls,
336
+ error:
337
+ "create-evergreen-campaigns prompt is missing connection-only guidance",
338
+ startedAt,
339
+ completedAt: new Date().toISOString(),
340
+ };
341
+ }
303
342
  } catch (err) {
304
343
  const error = err instanceof Error ? err.message : String(err);
305
344
  calls.push({
@@ -332,6 +371,37 @@ async function verifyCreateCampaignSmoke({
332
371
  };
333
372
  }
334
373
 
374
+ function verifyConnectionOnlyEvergreenContract(tools) {
375
+ const setupTool = (tools || []).find(
376
+ (tool) => tool?.name === "setup_evergreen_campaigns"
377
+ );
378
+ const schema = setupTool?.inputSchema || {};
379
+ const properties = schema.properties || {};
380
+ const sequenceOptions = properties.campaignSequenceOptions || {};
381
+ const mode = sequenceOptions.properties?.mode || {};
382
+ const issues = [];
383
+
384
+ if (!properties.workspaceId) {
385
+ issues.push("setup_evergreen_campaigns.workspaceId");
386
+ }
387
+ if (!properties.campaignSequenceOptions) {
388
+ issues.push("setup_evergreen_campaigns.campaignSequenceOptions");
389
+ }
390
+ if (!Array.isArray(mode.enum) || !mode.enum.includes("connection_only")) {
391
+ issues.push("setup_evergreen_campaigns.campaignSequenceOptions.mode");
392
+ }
393
+ if (sequenceOptions.additionalProperties !== false) {
394
+ issues.push(
395
+ "setup_evergreen_campaigns.campaignSequenceOptions.additionalProperties"
396
+ );
397
+ }
398
+
399
+ return {
400
+ ok: issues.length === 0,
401
+ issues,
402
+ };
403
+ }
404
+
335
405
  function summarizeAttempt(result, attempt) {
336
406
  return {
337
407
  attempt,
@@ -339,6 +409,12 @@ function summarizeAttempt(result, attempt) {
339
409
  availableToolCount: result.availableTools?.length || 0,
340
410
  missingToolCount: result.missingTools?.length || 0,
341
411
  forbiddenToolCount: result.forbiddenTools?.length || 0,
412
+ connectionOnlyEvergreenContract: result.connectionOnlyEvergreenContract
413
+ ? {
414
+ ok: result.connectionOnlyEvergreenContract.ok === true,
415
+ issues: result.connectionOnlyEvergreenContract.issues || [],
416
+ }
417
+ : null,
342
418
  error: result.error || null,
343
419
  createCampaignSmoke: result.createCampaignSmoke
344
420
  ? {
@@ -410,6 +486,7 @@ async function verifySellableMcpRuntimeOnce({
410
486
  let missingTools = [...requiredTools];
411
487
  let forbiddenTools = [];
412
488
  let createCampaignSmoke = null;
489
+ let connectionOnlyEvergreenContract = { ok: false, issues: [] };
413
490
  let error = null;
414
491
 
415
492
  try {
@@ -423,6 +500,8 @@ async function verifySellableMcpRuntimeOnce({
423
500
  forbiddenTools = availableTools.filter((tool) =>
424
501
  FORBIDDEN_SELLABLE_MCP_TOOLS.includes(tool)
425
502
  );
503
+ connectionOnlyEvergreenContract =
504
+ verifyConnectionOnlyEvergreenContract(toolList.tools);
426
505
  if (forbiddenTools.length > 0) {
427
506
  createCampaignSmoke = {
428
507
  ok: false,
@@ -466,6 +545,7 @@ async function verifySellableMcpRuntimeOnce({
466
545
  !error &&
467
546
  missingTools.length === 0 &&
468
547
  forbiddenTools.length === 0 &&
548
+ connectionOnlyEvergreenContract.ok === true &&
469
549
  createCampaignSmoke?.ok === true,
470
550
  skipped: false,
471
551
  command,
@@ -474,6 +554,7 @@ async function verifySellableMcpRuntimeOnce({
474
554
  availableTools,
475
555
  missingTools,
476
556
  forbiddenTools,
557
+ connectionOnlyEvergreenContract,
477
558
  createCampaignSmoke,
478
559
  stderrTail: stderrLines,
479
560
  error: error ? redact(error) : null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.310",
3
+ "version": "0.1.312",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {
@@ -134,6 +134,18 @@ run without user input, or similar, run in **automation mode**:
134
134
  exactly one quality-valid route-proof row when needed, and attach the
135
135
  recommended sequence, but it still must not launch campaigns, schedule
136
136
  sends, send messages, or spend paid InMail.
137
+ - **Connection-only evergreen completion is optional and non-default.** Use it
138
+ only when the prompt explicitly says connection-only, invite-only,
139
+ connection request only, no DMs, or no follow-ups. Pass the typed option
140
+ `campaignSequenceOptions:{ mode:"connection_only" }` plus an explicit
141
+ `workspaceId` to `setup_evergreen_campaigns`. Omit
142
+ `campaignSequenceOptions` for the standard tier-recommended Premium,
143
+ Sales Nav, Recruiter, and paid InMail behavior.
144
+ Connection-only completion still creates/reuses the same evergreen lanes,
145
+ source rows, and paused/unlaunched review state, but it attaches only the
146
+ canonical invite template. It does not require Message Drafting, generated
147
+ messages, route-proof approval, first DM, InMail, View Profile fallback,
148
+ follow-up branch, `currentStep:"send"`, launch, scheduling, or sends.
137
149
 
138
150
  If the invoking prompt explicitly asks for interactive message polish or sample
139
151
  proof, run in **interactive polish mode** and use the confirmation/sample steps
@@ -720,6 +732,9 @@ not a parent-thread summary. The receipt must include:
720
732
  creation, preserve that original packet only in a separate `parentLanePacket`
721
733
  object; do not leave `setupPlanCall.campaignId:null` or
722
734
  `setupPlanCall.tableId:null`.
735
+ If the parent plan used connection-only, the receipt must also include
736
+ `setupPlanCall.campaignSequenceOptions:{ "mode":"connection_only" }` or
737
+ `setupPlanCall.sequencePolicy.mode:"connection_only"`.
723
738
  Short form: For create lanes, `setupPlanCall.campaignId` and `setupPlanCall.tableId` must be the actual created campaign/table ids.
724
739
  Short form: Do not leave `setupPlanCall.campaignId:null` or `setupPlanCall.tableId:null`.
725
740
  - `createCampaignWorkflowReceipt`: proof the worker loaded the actual installed
@@ -810,21 +825,39 @@ not a parent-thread summary. The receipt must include:
810
825
  them into canonical `promptLoadedToHasMoreFalse`, `requiredAssetsLoaded`,
811
826
  `validationLoaded`, `reviewBatchRowHash`, and
812
827
  `messageDraftRecommendation`.
828
+ Connection-only lanes are the exception: do not include
829
+ `messageDraftingReceipt` or run generated-message prep unless the operator
830
+ separately asked for message copy. The parent verifier expects no message
831
+ proof for connection-only lanes.
813
832
  - `reviewBatchReceipt`: review-batch row ids/hash, generated row count,
814
833
  quality-valid route-proof row id when approved, and proof that no broad
815
834
  approve-all occurred.
835
+ Connection-only lanes are the exception: do not approve a route-proof row and
836
+ do not include a required review-batch receipt.
816
837
  - `sequenceReceipt`: exact current workflowTableId, recommended non-paid
817
838
  sequence attach/precheck result, and readback showing `hasSequence:true` when
818
839
  completion is claimed. If the tool output uses
819
840
  `attachRecommendedSequenceResult.actionTypes` or
820
841
  `nonPaidRecommendedSequence`, copy them to canonical `actionTypes` and
821
842
  `nonPaid`.
843
+ For connection-only lanes, do not call `attach_recommended_sequence`. Attach
844
+ with `attach_sequence({ tableId, templateRef:"connection_only" })`, then
845
+ include exact current-template proof:
846
+ `sequenceReceipt.hasSequence:true`,
847
+ `sequenceReceipt.actionTypes:["send_invite"]`, and
848
+ `sequenceReceipt.nonPaid:true`. Receipts that only say
849
+ `tool:"attach_recommended_sequence"` or include `send_dm`,
850
+ `send_inmail_open`, `send_inmail_closed`, or `view_profile` are failing
851
+ connection-only receipts.
822
852
  - final paused-send proof: if the current campaign table is `DRAFT` after the
823
853
  sequence is attached, call the product `pause_campaign({ campaignId })`
824
854
  endpoint/tool to put the unlaunched campaign into `PAUSED` review state, then
825
855
  reread the campaign/table. Do not raw-write `campaignStatus`, do not start or
826
856
  launch, and do not schedule/send. Completion requires reread proof of
827
- `currentStep:"send"` and `campaignStatus:"PAUSED"`. If the raw output nests
857
+ `currentStep:"send"` and `campaignStatus:"PAUSED"` for standard
858
+ customer-visible lanes. Connection-only lanes may finish at a paused
859
+ unlaunched review step such as `currentStep:"review"` as long as the exact
860
+ invite-only sequence proof and source-row proof pass. If the raw output nests
828
861
  this in `finalCampaignRead` or `finalTableRead`, copy it to canonical
829
862
  `finalPausedSendProof.currentStep` and
830
863
  `finalPausedSendProof.campaignStatus`.
@@ -1088,9 +1121,14 @@ plain row generation. The lane worker must inline the same
1088
1121
  approvedGeneratedMessageCount exactly 1. Do not report completion with 2+
1089
1122
  approved rows.
1090
1123
  Approval shorthand: Do not report completion with 2+ approved rows.
1091
- 8. Continue to `attach_recommended_sequence({ campaignId, currentStep:"send" })`
1092
- and, if the campaign is still `DRAFT`, `pause_campaign({ campaignId })`.
1093
- Reread the campaign/table before claiming completion.
1124
+ 8. Standard mode: continue to
1125
+ `attach_recommended_sequence({ campaignId, currentStep:"send" })` and, if
1126
+ the campaign is still `DRAFT`, `pause_campaign({ campaignId })`. Reread the
1127
+ campaign/table before claiming completion.
1128
+ Connection-only mode: skip Message Drafting and route-proof approval, call
1129
+ `attach_sequence({ tableId, templateRef:"connection_only" })`, prove
1130
+ `sequenceReceipt.actionTypes:["send_invite"]`, and pause/reread without
1131
+ launching or sending.
1094
1132
 
1095
1133
  That packaged worker path must return `messageDraftingReceipt.statusSource:
1096
1134
  "packaged-generate-messages-worker"`. It is accepted only when the receipt
@@ -1515,11 +1553,14 @@ Message, and verify current-revision sample messages before final completion.
1515
1553
  - DM lanes: add a `Delivery format:` line — either `multiline (each paragraph sends as its own DM message)` or `single message`. When multiline, the template's blank-line paragraphs ARE the message boundaries — write each one as a standalone typed message.
1516
1554
  - **InMail lanes can never be multiline**: an InMail is one message and the recipient must reply before anything else can be sent. InMail-bound templates must read as one cohesive message — declare `Delivery format: single message (InMail — no follow-up until reply)` and never structure the copy to depend on multi-message pacing.
1517
1555
 
1518
- - The sequence is auto-selected by sender tier; do not hand-author sequence
1556
+ - In standard mode, the sequence is auto-selected by sender tier; do not hand-author sequence
1519
1557
  templates here. Sales Nav/Recruiter senders may receive the unified Sales
1520
1558
  Nav cascade through `attach_recommended_sequence`; attaching it does not
1521
1559
  spend paid InMail credits by itself. Do not substitute the manual Paid
1522
1560
  InMail Campaign template.
1561
+ For explicit connection-only evergreen lanes only, use the backend-owned
1562
+ `attach_sequence({ tableId, templateRef:"connection_only" })` path and do
1563
+ not call `attach_recommended_sequence`.
1523
1564
  3. **Customer-Visible Completion Contract**: a named evergreen lane that appears
1524
1565
  as a campaign card or campaign-backed table is not done when the shell exists.
1525
1566
  It is done only when the customer can open the campaign and land on final
@@ -1543,7 +1584,7 @@ Message, and verify current-revision sample messages before final completion.
1543
1584
  evergreen setup. Do not leave customer-visible campaigns at
1544
1585
  `filter-choice`, `filter-rules`, or `apply-icp-rubric` and report success.
1545
1586
  Do not proceed to Message Drafting until saved filters are applied.
1546
- - Message Drafting has run from the current campaign/table basis, using the
1587
+ - In standard mode, Message Drafting has run from the current campaign/table basis, using the
1547
1588
  create-campaign message prompt/assets and validation gate. Updating
1548
1589
  `currentStep:"messages"` is not proof. Parent-thread handwritten copy is
1549
1590
  not a substitute. The proof receipt must show
@@ -1584,6 +1625,9 @@ Message, and verify current-revision sample messages before final completion.
1584
1625
  Fallback samples must still reject source/conversation hedges such as
1585
1626
  `"hope this is relevant"`, `"might be interested"`, or `"saw you in a few
1586
1627
  conversations"`.
1628
+ For explicit connection-only lanes, this whole Message Drafting and sample
1629
+ proof block is not required and must not be faked with parent-written
1630
+ messages.
1587
1631
  - The first review batch exists and at least 3 review rows have generated
1588
1632
  messages from the approved brief. If fewer than 3 usable rows exist, report
1589
1633
  the actual count and why.
@@ -1596,7 +1640,7 @@ Message, and verify current-revision sample messages before final completion.
1596
1640
  completion, approve exactly one quality-valid generated row. If one or more
1597
1641
  rows are already approved, do not add more approvals during evergreen
1598
1642
  completion. Never broad approve all rows.
1599
- - The recommended tier-aware sequence is attached to the current campaign
1643
+ - In standard mode, the recommended tier-aware sequence is attached to the current campaign
1600
1644
  table, and the watched campaign is on Send. Use
1601
1645
  `attach_recommended_sequence({ campaignId, currentStep:"send" })` when a
1602
1646
  safe attach is needed. After `confirm_lead_list` or any source-list copy,
@@ -1611,6 +1655,10 @@ Message, and verify current-revision sample messages before final completion.
1611
1655
  manual Paid InMail Campaign, and never attach/replace sequence outside the
1612
1656
  current table unless the current lane packet explicitly allowed sequence
1613
1657
  repair.
1658
+ For explicit connection-only lanes, the current campaign table must have
1659
+ exactly the canonical invite-only sequence:
1660
+ `sequenceReceipt.actionTypes:["send_invite"]`. Any DM, InMail, or View
1661
+ Profile action means the lane is not complete.
1614
1662
  - If the current campaign table is still `DRAFT` after sequence/readiness
1615
1663
  proof, call `pause_campaign({ campaignId })` and reread. `pause_campaign`
1616
1664
  is the product-native review-state transition; it is not a launch and does
@@ -62,6 +62,11 @@ for lease expiry; never guess a fence.
62
62
  The default `intent:"auto"` inspects `managed_waterfall`,
63
63
  `dashboard_evergreen`, and `active_campaign` lane sources. Report the lane
64
64
  source and lane chain as proof for every selected sender.
65
+ Connection-only evergreen lanes are invite-only refill targets. When the packet
66
+ or campaign proof shows exactly `["send_invite"]`, report and execute only
67
+ `selectedLane:"send_invite"` work; do not infer DM, InMail, Sales Nav cascade,
68
+ paid-credit refresh, message generation, follow-up, sequence mutation,
69
+ launch/start beyond packet authority, scheduler writes, or direct sends.
65
70
 
66
71
  If membership blocks a workspace read, report the structured
67
72
  `workspace_access` blocker instead of retrying auth or switching workspaces.
@@ -196,6 +196,11 @@ rather than asking which campaign class to fill. If a stale target plan selects
196
196
  planner before mutation.
197
197
  Short form: trust the target plan's inferred lane when it is a connection invite,
198
198
  paid-InMail refill lane, or unified Sales Nav cascade.
199
+ Connection-only evergreen campaigns with exact `["send_invite"]` sequence proof
200
+ are plain invite capacity: treat them as `selectedLane:"send_invite"` only. Do
201
+ not infer DM, InMail, Sales Nav cascade, paid-credit refresh, message
202
+ generation, follow-up, sequence mutation, launch/start, scheduling override, or
203
+ direct-send work from a connection-only lane.
199
204
 
200
205
  Structured planner packet:
201
206
 
@@ -19,10 +19,9 @@ allowed-tools:
19
19
  - mcp__sellable__get_campaign_table_schema
20
20
  - mcp__sellable__list_tables
21
21
  - mcp__sellable__get_rows_minimal
22
+ - mcp__sellable__refresh_sender_engagement
22
23
  - mcp__sellable__fetch_linkedin_posts
23
- - mcp__sellable__select_promising_posts
24
24
  - mcp__sellable__fetch_post_engagers
25
- - mcp__sellable__add_on_demand_leads
26
25
  ---
27
26
 
28
27
  # Refresh Sender Engagement
@@ -34,7 +33,7 @@ You are a pipeline supply agent. People who engage with a sender's LinkedIn post
34
33
  <inputs>
35
34
  The invoking prompt names the senders ("refresh sender engagement for csreyes92 and thomas"). Resolve each via `list_senders` (match name/handle/LinkedIn URL). With no names given, inspect the active workspace and refresh every connected sender that has an active/paused sender-owned Post Engagers campaign backed by Signal Discovery.
36
35
 
37
- Optional: lookback window (default: posts from the last 30 days), engagement sources (default `both` reactions+comments), target sender names/ids, maximum posts per sender (default 5, hard cap 5 unless the user explicitly asks for more), and target campaign ids/names when the user wants to force a specific campaign.
36
+ Optional: target sender names/ids, `tableId` when the user wants to force a specific campaign table, maximum posts per sender (default 5, hard cap 5 unless the user explicitly asks for more), and maximum engager pages per tracked post.
38
37
  </inputs>
39
38
 
40
39
  <entrypoint>
@@ -53,39 +52,36 @@ suggest `refill-sends`.
53
52
  <objective>
54
53
  For each target sender/campaign:
55
54
 
56
- 1. **Find active Signal Discovery/Post Engagers campaigns first**:
55
+ 1. **Authenticate and resolve workspace**:
57
56
  - Call `get_auth_status` and confirm the active workspace.
58
- - Call `get_campaigns` and, when needed, `list_tables`/`get_campaign` to find candidate campaigns.
59
- - Keep only active or paused campaign-backed sender-owned Post Engagers campaigns. Strong signals include campaign/table names like `<Sender> - Post Engagers`, `sourceProvider:"signal-discovery"`, sender attachment to exactly one sender, and source/list/table readback indicating post engagers.
60
- - Exclude shared Signal Discovery lanes, Shared Cold Fallback lanes, archived/completed campaigns, non-campaign tables, direct/on-demand-only campaigns, and any campaign attached to multiple senders unless the user explicitly selected it and the readback proves it is sender-owned.
61
- - If no matching campaign exists for a sender, report "no active Post Engagers Signal Discovery campaign — create one first" and skip; do not silently create campaigns.
62
- 2. **Resolve the sender and source boundary**:
63
- - Match each candidate campaign to its sender from `list_senders`/`get_sender`.
64
- - Use the sender's own LinkedIn profile URL/handle as the only source author boundary.
65
- - Never scrape third-party authors into a sender-owned Post Engagers campaign.
66
- 3. **Find relevant sender-authored posts to refresh**:
67
- - Call `fetch_linkedin_posts({ linkedinUrl: sender profile, limit: 25 })`.
68
- - Keep original posts authored by that exact sender, not reposts, from the lookback window.
69
- - Rank posts by recency, engagement count, and fit to the campaign's buyer/problem/topic. Prefer posts likely to attract the target buyer over generic company updates.
70
- - If the campaign has an existing Signal Discovery/source state that names tracked/selected posts, prefer refreshing those posts when they are still inside the lookback window and relevant; otherwise choose the strongest recent sender-authored posts.
71
- - When the product flow expects selected posts to be visible, call `select_promising_posts` before scraping.
72
- 4. **Pull latest engagers**:
73
- - For the top posts (up to 5 per sender per run), call `fetch_post_engagers({ postUrl, sources })`.
74
- - Default `sources` to `"both"` unless the user requested reactions-only or comments-only.
75
- 5. **Filter to ICP** using the campaign's existing headline ICP criteria (from `get_campaign` brief/rubrics/table schema). Judge each engager's headline against those criteria; exclude obvious non-fits, the sender's own colleagues, existing employees, students/job-seekers, competitors, and anyone with no usable headline. When the campaign has no criteria, keep likely decision-makers/operators and exclude weak-fit profiles.
76
- 6. **Add net-new leads only**:
77
- - Use `add_on_demand_leads({ tableId, leads, skipDuplicates: true })` with name, headline-derived title, profile URL, and source/post context where the tool accepts it.
78
- - Dedupe is mandatory. Never disable dedupe on a scheduled run.
79
- - Do not generate, approve, schedule, or send messages.
80
- 7. **Report**: campaigns inspected, target campaigns selected, posts scanned, engagers found, ICP-passing, net-new added per sender/campaign. If a sender posted nothing in the window, say "no recent posts — nothing to refresh" (that is a truthful no-op, not a failure).
57
+ - If there is no active workspace or auth is missing, stop with the MCP guidance. Do not use local env files or repo scripts as a substitute for MCP auth.
58
+ - Resolve each target sender with `list_senders`/`get_sender`. Use the sender id returned by the product, not a guessed handle.
59
+ 2. **Use the typed product command first**:
60
+ - Call `refresh_sender_engagement` in `mode:"dry_run"` for each sender.
61
+ - Always pass `workspaceId`, `senderId`, and any user-specified `tableId`, `maxPosts`, or `maxEngagerPages`.
62
+ - Treat the dry-run response as the campaign/source boundary authority. It should identify the sender-owned Post Engagers slot, campaign, workflow table, source provider `campaign-tracked-post`, source table type `tracked_post_engager_source_list`, expected tracked posts, expected engager refresh/import work, and a `dryRunFingerprint`.
63
+ - If the typed command reports no eligible sender-owned Post Engagers campaign, no tracked posts, no recent posts, or a workspace/sender mismatch, report that exact blocker and stop for that sender. Do not silently create campaigns or fall back to shared lanes.
64
+ 3. **Gate writes with dry-run proof**:
65
+ - Never call `refresh_sender_engagement` in `mode:"apply"` before a successful dry run from this same run.
66
+ - For manual runs, show the target workspace, sender, campaign/table, source lead list, expected tracked posts, expected row/import impact, and `dryRunFingerprint`, then wait for explicit user approval before apply.
67
+ - For scheduled automations where the user has already authorized refreshes, apply is allowed only with the exact `dryRunFingerprint` from the immediately preceding dry run.
68
+ - If apply rejects the fingerprint as stale or mismatched, rerun dry-run and ask for approval again for manual runs.
69
+ 4. **Apply through the typed command only**:
70
+ - Call `refresh_sender_engagement({ mode:"apply", workspaceId, senderId, tableId?, maxPosts?, maxEngagerPages?, dryRunFingerprint })`.
71
+ - The apply path must materialize/refresh tracked posts, import deduped tracked-post engagers into the campaign table, and preserve product idempotency. Do not use `add_on_demand_leads` or raw table row writes for the same operation.
72
+ 5. **Read-only fallback is diagnostic only**:
73
+ - If the typed command is unavailable, use `get_subskill_prompt`/`search_subskill_prompts` and stop with `blocked: missing_refresh_sender_engagement_tool`.
74
+ - `fetch_linkedin_posts` and `fetch_post_engagers` may be used only to explain source availability or diagnose provider issues. They are not an alternate write path.
75
+ 6. **Report**: campaigns inspected, target campaign/table, tracked posts materialized/refreshed, engagers scanned, ICP-passing/importable, net-new rows imported, duplicates skipped, and any no-op reason per sender/campaign. If a sender posted nothing in the window, say "no recent posts — nothing to refresh" (that is a truthful no-op, not a failure).
81
76
  </objective>
82
77
 
83
78
  <safety>
84
- - LinkedIn operations here are read-only fetches plus adding rows to a campaign table. **No messages are generated, approved, or sent by this skill.**
79
+ - LinkedIn operations here are read-only fetches plus a typed product import into a campaign table. **No messages are generated, approved, scheduled, or sent by this skill.**
85
80
  - Respect workspace boundaries: only add leads to campaigns in the active workspace, and only for senders that belong to it.
86
81
  - Respect campaign boundaries: only add engagers to the matched sender-owned Post Engagers campaign/table. Do not mix shared-lane engagers into sender-owned campaigns or sender-owned engagers into shared lanes.
87
82
  - Cap provider usage per run: at most 5 posts × `fetch_post_engagers` per sender. If the invoking automation wants more, it must say so explicitly.
88
- - Never call `start_campaign`, `attach_sequence`, `queue_campaign_cells`, `start_campaign_message_preparation`, approval tools, or any send/schedule tool.
83
+ - Never call `start_campaign`, `attach_sequence`, `queue_campaign_cells`, `start_campaign_message_preparation`, approval tools, send/schedule tools, or shared-lane mutation tools.
84
+ - Never create campaigns, change campaign status, change sender ownership, approve campaign cells, or start message prep as part of this refresh.
89
85
  </safety>
90
86
 
91
87
  <output>