@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 +0 -21
- package/bin/sellable-install.mjs +16 -160
- package/lib/runtime-verify.mjs +82 -1
- package/package.json +1 -1
- package/skill-templates/create-evergreen-campaigns.md +55 -7
- package/skill-templates/refill-sends-v2.md +5 -0
- package/skill-templates/refill-sends.md +5 -0
- package/skill-templates/refresh-sender-engagement.md +25 -29
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:
|
package/bin/sellable-install.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
writeFileSync,
|
|
14
14
|
} from "node:fs";
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
|
-
import {
|
|
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
|
|
2149
|
-
|
|
2150
|
-
|
|
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.
|
|
2172
|
-
|
|
2173
|
-
|
|
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);
|
package/lib/runtime-verify.mjs
CHANGED
|
@@ -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:
|
|
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
|
@@ -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"
|
|
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.
|
|
1092
|
-
|
|
1093
|
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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:
|
|
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. **
|
|
55
|
+
1. **Authenticate and resolve workspace**:
|
|
57
56
|
- Call `get_auth_status` and confirm the active workspace.
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
|
|
61
|
-
-
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
-
|
|
75
|
-
|
|
76
|
-
6. **
|
|
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
|
|
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,
|
|
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>
|