@sellable/install 0.1.316 → 0.1.318

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,20 +5,18 @@ access.
5
5
 
6
6
  ## Default Rule
7
7
 
8
- For Sellable-managed customer channels, use one shared Sellable Reply Bot
9
- Socket Mode listener and route inbound events by exact channel id. Do not run
10
- multiple active Hermes profile gateways against the same `SLACK_APP_TOKEN`.
8
+ For Sellable-managed customer channels, use one dedicated Slack app/token pair
9
+ per active Hermes customer profile. Run native `hermes -p <profile> gateway run`
10
+ for that profile only. Do not run multiple active Hermes profile gateways
11
+ against the same `SLACK_APP_TOKEN`.
11
12
 
12
- The shared dispatcher maps `slackTeamId + channelId` to `customerSlug`,
13
- `workspaceId`, `hermesProfile`, and `responsePolicy`. It then invokes
14
- `hermes -p <profile>` with profile-local `SELLABLE_CONFIG_PATH` and
15
- `SELLABLE_CONFIGS_DIR`, and posts the response back to the original Slack
16
- channel/thread.
13
+ The customer's Slack app is bound to the exact Slack channel id and the
14
+ customer's profile-local Sellable MCP config. Customer profiles must not store
15
+ or read Sellable Admin MCP credentials, admin Slack app tokens, or sibling
16
+ profile config.
17
17
 
18
- Customer profiles do not store the shared raw Slack token pair; route records
19
- store metadata and secret references only. Use dedicated per-customer Slack
20
- app/token pairs only for customer-owned Slack workspaces or strict enterprise
21
- isolation.
18
+ The shared dispatcher remains a fallback/research path only. Use it only when
19
+ Sellable Admin explicitly owns one shared listener and route registry.
22
20
 
23
21
  The 2026-07-06 Hostinger POC proved the native Hermes listener for the
24
22
  `sellable-admin` profile with the existing `Sellable Reply Bot` app in
@@ -35,8 +33,9 @@ hermes --profile acme send --to slack:C0BFERDV3N0 --subject '[Hermes acme smoke]
35
33
  ```
36
34
 
37
35
  That succeeded and is useful as an internal smoke test. It does not change the
38
- inbound listener rule: one shared dispatcher should own Socket Mode for
39
- Sellable-managed channels.
36
+ inbound listener rule: each active customer profile needs its own native Slack
37
+ app token pair unless Sellable Admin deliberately chooses the dispatcher
38
+ fallback.
40
39
 
41
40
  ## Which Slack Values Are Enough
42
41
 
@@ -114,17 +113,32 @@ SLACK_REQUIRE_MENTION=true
114
113
  Keep the file mode at `0600`. Do not copy raw tokens into planning docs, chat,
115
114
  or committed artifacts. Record token fingerprints only.
116
115
 
116
+ The profile's Hermes MCP env must also include the profile-local Sellable
117
+ config, customer workspace lock, and fail-closed guard:
118
+
119
+ ```yaml
120
+ SELLABLE_CONFIG_PATH: /srv/hermes/profiles/acme/sellable/config.json
121
+ SELLABLE_CONFIGS_DIR: /srv/hermes/profiles/acme/sellable/configs
122
+ SELLABLE_LOCK_WORKSPACE_ID: ws_acme
123
+ SELLABLE_REQUIRE_WORKSPACE_LOCK: "1"
124
+ ```
125
+
126
+ `SELLABLE_REQUIRE_WORKSPACE_LOCK=1` means an unbound customer profile fails
127
+ closed before making Sellable API calls instead of falling back to shared or
128
+ admin config state. Only the `sellable-admin` profile should provision,
129
+ inspect, or repair other profiles.
130
+
117
131
  For the shared dispatcher model, do not copy the shared `SLACK_BOT_TOKEN` or
118
132
  `SLACK_APP_TOKEN` into customer profile `.env` files. The dispatcher owns those
119
133
  tokens and route metadata decides the customer profile.
120
134
 
121
135
  ## Shared Dispatcher Ownership
122
136
 
123
- The shared Slack dispatcher and its route registry are internal Sellable Admin
124
- surfaces. This customer-facing installer must not create Slack apps, write
125
- shared listener tokens, or validate cross-customer route bundles. Sellable Admin
126
- owns those steps, then calls this installer only for profile-local Sellable
127
- runtime bootstrap inside the already-provisioned Hermes profile.
137
+ The shared Slack dispatcher and its route registry are fallback Sellable Admin
138
+ surfaces. This customer-facing installer must not create shared route bundles or
139
+ write shared listener tokens. Sellable Admin owns Slack app creation and profile
140
+ provisioning, then calls this installer for profile-local Sellable runtime
141
+ bootstrap inside the already-provisioned Hermes profile.
128
142
 
129
143
  ## Verification
130
144
 
@@ -147,8 +161,9 @@ Expected proof:
147
161
 
148
162
  ## Current Phase 03 Status
149
163
 
150
- Phase 03 has proved the profile-scoped installer path, outbound Hermes send
151
- path, and live Slack listener for the `sellable-admin` profile. Phase 03.1 adds
152
- the shared dispatcher route registry as a Sellable Admin-owned onboarding
153
- surface so managed customer channels can be onboarded through the VPS without
154
- local-only browser state, manual SSH edits, or untracked processes.
164
+ Phase 03 proved the profile-scoped installer path, outbound Hermes send path,
165
+ and live Slack listener for the `sellable-admin` profile. Phase 03.1 now targets
166
+ dedicated native Slack apps per customer profile, profile-local Sellable MCP
167
+ locks, and fail-closed customer profile behavior so managed customer channels
168
+ can be onboarded through the VPS without local-only browser state, manual SSH
169
+ edits, or untracked processes.
package/README.md CHANGED
@@ -122,11 +122,13 @@ It creates:
122
122
  /srv/hermes/profiles/acme/.env # only when Slack token inputs are supplied
123
123
  ```
124
124
 
125
- Bootstrap also writes `SELLABLE_LOCK_WORKSPACE_ID=<workspace-id>` into the
126
- profile's Hermes MCP env. Customer profiles are therefore locked to their own
127
- Sellable workspace: Sellable MCP requests are forced to that workspace,
128
- workspace switching/creation is blocked, and workspace listings are filtered.
129
- Use the `sellable-admin` profile for provisioning or cross-profile inspection.
125
+ Bootstrap also writes `SELLABLE_LOCK_WORKSPACE_ID=<workspace-id>` and
126
+ `SELLABLE_REQUIRE_WORKSPACE_LOCK=1` into the profile's Hermes MCP env.
127
+ Customer profiles are therefore locked to their own Sellable workspace:
128
+ Sellable MCP requests are forced to that workspace, workspace switching/creation
129
+ is blocked, workspace listings are filtered, and a missing lock fails closed
130
+ instead of falling back to shared/admin config. Use the `sellable-admin` profile
131
+ for provisioning or cross-profile inspection.
130
132
 
131
133
  See [`HERMES-SLACK-PROFILE-SCOPING.md`](./HERMES-SLACK-PROFILE-SCOPING.md) for
132
134
  the customer Slack isolation contract and Socket Mode token requirements.
@@ -26,8 +26,8 @@
26
26
  "ownership": "message strategy, proof inventory, token rules, skeptical-prospect review, and selected winner only",
27
27
  "codex": {
28
28
  "description": "Message Drafting worker for campaign-backed template proposals after confirm_lead_list imports a non-empty bounded review batch.",
29
- "model": "gpt-5.5",
30
- "modelReasoningEffort": "xhigh",
29
+ "model": "gpt-5.6-sol",
30
+ "modelReasoningEffort": "high",
31
31
  "sandboxMode": "read-only",
32
32
  "nicknameCandidates": [
33
33
  "Message Drafting",
@@ -179,6 +179,8 @@ Options:
179
179
  --server <mode> package, local, or hosted. Default: package
180
180
  --token <token> Sellable API token. Also reads SELLABLE_TOKEN.
181
181
  --workspace-id <id> Sellable workspace id. Also reads SELLABLE_WORKSPACE_ID.
182
+ --require-workspace-lock For Hermes customer profiles, fail closed unless
183
+ SELLABLE_LOCK_WORKSPACE_ID is present in MCP env.
182
184
  --api-url <url> Sellable API URL. Default: ${DEFAULT_API_URL}
183
185
  --sellable-config-path <path>
184
186
  Profile-scoped Sellable config path. Also reads SELLABLE_CONFIG_PATH.
@@ -222,7 +224,9 @@ Hermes profile bootstrap:
222
224
  exact channel id unless --slack-allowed-channels is supplied. Use
223
225
  customer-scoped Sellable credentials and customer-scoped Slack credentials for
224
226
  customer profiles; shared admin tokens are weaker isolation and should remain
225
- internal-only.
227
+ internal-only. Bootstrap writes SELLABLE_REQUIRE_WORKSPACE_LOCK=1 whenever a
228
+ workspace id is supplied, so a customer profile fails closed if the workspace
229
+ lock is later removed or not yet provisioned.
226
230
 
227
231
  Hermes shared Slack dispatcher:
228
232
  Shared listener setup and route-registry validation are internal Sellable
@@ -1656,7 +1660,7 @@ Treat host capabilities as concrete functions, not prose conventions:
1656
1660
  import and before dispatching Message Drafting only.
1657
1661
  - \`launch_message_drafting\`: Claude Code uses \`Task\` with \`subagent_type\`
1658
1662
  \`post-find-leads-message-scout\` when listed; Codex uses the returned
1659
- compatibility agent or a generic \`gpt-5.5\` / \`xhigh\` Message Drafting agent.
1663
+ compatibility agent or a generic \`gpt-5.6-sol\` / \`high\` Message Drafting agent.
1660
1664
 
1661
1665
  If a required interactive question function or MCP loader is missing, stop and
1662
1666
  explain the Sellable install/reload problem. Source and filter work use
@@ -3053,8 +3057,8 @@ function generateCodexAgentToml(agent) {
3053
3057
  const codex = agent.codex;
3054
3058
  return `name = ${quoteToml(agent.name)}
3055
3059
  description = ${quoteToml(codex.description)}
3056
- model = ${quoteToml(codex.model || "gpt-5.5")}
3057
- model_reasoning_effort = ${quoteToml(codex.modelReasoningEffort || "xhigh")}
3060
+ model = ${quoteToml(codex.model || "gpt-5.6-sol")}
3061
+ model_reasoning_effort = ${quoteToml(codex.modelReasoningEffort || "high")}
3058
3062
  sandbox_mode = ${quoteToml(codex.sandboxMode || "read-only")}
3059
3063
  nickname_candidates = ${tomlArray(codex.nicknameCandidates || [agent.displayName])}
3060
3064
  developer_instructions = ${tomlMultilineString(agent.prompt)}
@@ -3517,6 +3521,9 @@ function mcpEnvForHost(host, opts) {
3517
3521
  if (opts.lockWorkspaceId) {
3518
3522
  env.SELLABLE_LOCK_WORKSPACE_ID = opts.lockWorkspaceId;
3519
3523
  }
3524
+ if (opts.requireWorkspaceLock) {
3525
+ env.SELLABLE_REQUIRE_WORKSPACE_LOCK = "1";
3526
+ }
3520
3527
  return env;
3521
3528
  }
3522
3529
 
@@ -3815,6 +3822,7 @@ function parseHermesProfileBootstrapArgs(argv) {
3815
3822
  force: false,
3816
3823
  overwriteEnv: false,
3817
3824
  allowDuplicateSlackTokens: false,
3825
+ requireWorkspaceLock: false,
3818
3826
  verbose: false,
3819
3827
  };
3820
3828
 
@@ -3853,7 +3861,9 @@ function parseHermesProfileBootstrapArgs(argv) {
3853
3861
  else if (arg === "--json") opts.json = true;
3854
3862
  else if (arg === "--force") opts.force = true;
3855
3863
  else if (arg === "--overwrite-env") opts.overwriteEnv = true;
3856
- else if (arg === "--allow-duplicate-slack-tokens") {
3864
+ else if (arg === "--require-workspace-lock") {
3865
+ opts.requireWorkspaceLock = true;
3866
+ } else if (arg === "--allow-duplicate-slack-tokens") {
3857
3867
  opts.allowDuplicateSlackTokens = true;
3858
3868
  } else if (arg === "--verbose") opts.verbose = true;
3859
3869
  else {
@@ -4111,6 +4121,8 @@ function runHermesProfileBootstrap(argv) {
4111
4121
  const slackEnvKeys = Object.entries(slackEnv)
4112
4122
  .filter(([, value]) => value)
4113
4123
  .map(([key]) => key);
4124
+ const requireWorkspaceLock =
4125
+ parsed.requireWorkspaceLock || Boolean(parsed.workspaceId);
4114
4126
  const opts = {
4115
4127
  ...parsed,
4116
4128
  host: "hermes",
@@ -4121,6 +4133,7 @@ function runHermesProfileBootstrap(argv) {
4121
4133
  tokenSource,
4122
4134
  workspaceId: parsed.workspaceId,
4123
4135
  lockWorkspaceId: parsed.workspaceId,
4136
+ requireWorkspaceLock,
4124
4137
  apiUrl: parsed.apiUrl,
4125
4138
  };
4126
4139
 
@@ -4146,8 +4159,16 @@ function runHermesProfileBootstrap(argv) {
4146
4159
  activeWorkspaceId: parsed.workspaceId || null,
4147
4160
  activeWorkspaceName: parsed.workspaceName || null,
4148
4161
  workspaceLock: parsed.workspaceId
4149
- ? { enabled: true, workspaceId: parsed.workspaceId }
4150
- : { enabled: false, workspaceId: null },
4162
+ ? {
4163
+ enabled: true,
4164
+ workspaceId: parsed.workspaceId,
4165
+ required: requireWorkspaceLock,
4166
+ }
4167
+ : {
4168
+ enabled: false,
4169
+ workspaceId: null,
4170
+ required: requireWorkspaceLock,
4171
+ },
4151
4172
  tokenPresent: Boolean(token),
4152
4173
  tokenSource,
4153
4174
  tokenFingerprint: tokenFingerprint(token),
@@ -4319,6 +4340,11 @@ function runHermesProfileBootstrap(argv) {
4319
4340
  `Manual auth pending. Run: sellable auth set <token> --workspace-id <workspace_id> --sellable-config-path ${sellableConfigPath}`
4320
4341
  );
4321
4342
  }
4343
+ if (requireWorkspaceLock && !parsed.workspaceId) {
4344
+ summary.warnings.push(
4345
+ "Sellable MCP will fail closed until sellable-admin provisions SELLABLE_LOCK_WORKSPACE_ID for this customer profile."
4346
+ );
4347
+ }
4322
4348
 
4323
4349
  if (opts.json) {
4324
4350
  console.log(JSON.stringify(summary, null, 2));
@@ -45,6 +45,14 @@ export const REQUIRED_SELLABLE_MCP_TOOLS = [
45
45
 
46
46
  export const FORBIDDEN_SELLABLE_MCP_TOOLS = ["refill_campaign_sends"];
47
47
 
48
+ const CONNECTION_ONLY_EVERGREEN_PROMPT_CHUNK_LIMIT = 48_000;
49
+ const CONNECTION_ONLY_EVERGREEN_MAX_PROMPT_CHUNKS = 8;
50
+ const CONNECTION_ONLY_EVERGREEN_PROOF_NEEDLES = {
51
+ campaignSequenceOptions: 'campaignSequenceOptions:{ mode:"connection_only" }',
52
+ templateRef: 'templateRef:"connection_only"',
53
+ sendInviteReceipt: 'sequenceReceipt.actionTypes:["send_invite"]',
54
+ };
55
+
48
56
  export const CREATE_CAMPAIGN_SMOKE_CALLS = [
49
57
  {
50
58
  name: "get_auth_status",
@@ -84,6 +92,14 @@ export const CREATE_CAMPAIGN_SMOKE_CALLS = [
84
92
  limit: 1200,
85
93
  },
86
94
  },
95
+ {
96
+ name: "get_subskill_prompt",
97
+ arguments: {
98
+ subskillName: "create-evergreen-campaigns",
99
+ offset: 0,
100
+ limit: CONNECTION_ONLY_EVERGREEN_PROMPT_CHUNK_LIMIT,
101
+ },
102
+ },
87
103
  {
88
104
  name: "refill_sends",
89
105
  arguments: {
@@ -187,6 +203,43 @@ function parseJsonText(text) {
187
203
  }
188
204
  }
189
205
 
206
+ function emptyConnectionOnlyEvergreenProofs() {
207
+ return Object.fromEntries(
208
+ Object.keys(CONNECTION_ONLY_EVERGREEN_PROOF_NEEDLES).map((key) => [
209
+ key,
210
+ false,
211
+ ])
212
+ );
213
+ }
214
+
215
+ function getConnectionOnlyEvergreenProofs(prompt) {
216
+ return Object.fromEntries(
217
+ Object.entries(CONNECTION_ONLY_EVERGREEN_PROOF_NEEDLES).map(
218
+ ([key, needle]) => [key, prompt.includes(needle)]
219
+ )
220
+ );
221
+ }
222
+
223
+ function mergeConnectionOnlyEvergreenProofs(target, source) {
224
+ if (!source) return target;
225
+ for (const key of Object.keys(CONNECTION_ONLY_EVERGREEN_PROOF_NEEDLES)) {
226
+ target[key] = target[key] === true || source[key] === true;
227
+ }
228
+ return target;
229
+ }
230
+
231
+ function summarizeConnectionOnlyEvergreenProofs(proofs) {
232
+ const merged = { ...emptyConnectionOnlyEvergreenProofs(), ...(proofs || {}) };
233
+ const missing = Object.keys(CONNECTION_ONLY_EVERGREEN_PROOF_NEEDLES).filter(
234
+ (key) => merged[key] !== true
235
+ );
236
+ return {
237
+ ok: missing.length === 0,
238
+ proofs: merged,
239
+ missing,
240
+ };
241
+ }
242
+
190
243
  function summarizeToolResult(name, result) {
191
244
  const text = redact(textFromToolResult(result));
192
245
  const parsed = parseJsonText(text);
@@ -230,10 +283,26 @@ function summarizeToolResult(name, result) {
230
283
  }
231
284
 
232
285
  if (name === "get_subskill_prompt" && parsed) {
286
+ const prompt = typeof parsed.prompt === "string" ? parsed.prompt : "";
287
+ const connectionOnlyEvergreenProofs =
288
+ parsed.name === "create-evergreen-campaigns"
289
+ ? getConnectionOnlyEvergreenProofs(prompt)
290
+ : null;
233
291
  return {
234
292
  ...summary,
235
293
  subskillName: parsed.name || null,
236
- promptChars: typeof parsed.prompt === "string" ? parsed.prompt.length : 0,
294
+ promptChars: prompt.length,
295
+ promptLength:
296
+ typeof parsed.promptLength === "number" ? parsed.promptLength : null,
297
+ offset: typeof parsed.offset === "number" ? parsed.offset : null,
298
+ limit: typeof parsed.limit === "number" ? parsed.limit : null,
299
+ connectionOnlyEvergreenProofs,
300
+ connectionOnlyEvergreenGuidance:
301
+ parsed.name === "create-evergreen-campaigns"
302
+ ? summarizeConnectionOnlyEvergreenProofs(
303
+ connectionOnlyEvergreenProofs
304
+ ).ok
305
+ : null,
237
306
  hasMore: parsed.hasMore === true,
238
307
  nextOffset:
239
308
  typeof parsed.nextOffset === "number" ? parsed.nextOffset : null,
@@ -267,6 +336,7 @@ async function verifyCreateCampaignSmoke({
267
336
  .map((call) => call.name)
268
337
  .filter((name) => !available.has(name));
269
338
  const calls = [];
339
+ const connectionOnlyEvergreenProofs = emptyConnectionOnlyEvergreenProofs();
270
340
 
271
341
  if (missingTools.length > 0) {
272
342
  return {
@@ -280,67 +350,195 @@ async function verifyCreateCampaignSmoke({
280
350
  }
281
351
 
282
352
  for (const call of smokeCalls) {
283
- const callStartedAt = new Date().toISOString();
284
- try {
285
- const result = await client.callTool(
286
- { name: call.name, arguments: call.arguments },
287
- undefined,
288
- {
289
- timeout: timeoutMs,
290
- maxTotalTimeout: timeoutMs,
353
+ let nextCall = call;
354
+ let evergreenPromptChunks = 0;
355
+ const evergreenPromptOffsets = new Set();
356
+
357
+ while (nextCall) {
358
+ const callStartedAt = new Date().toISOString();
359
+ try {
360
+ const result = await client.callTool(
361
+ { name: nextCall.name, arguments: nextCall.arguments },
362
+ undefined,
363
+ {
364
+ timeout: timeoutMs,
365
+ maxTotalTimeout: timeoutMs,
366
+ }
367
+ );
368
+ const summary = summarizeToolResult(nextCall.name, result);
369
+ calls.push({
370
+ name: nextCall.name,
371
+ arguments: nextCall.arguments,
372
+ ok: summary.isError !== true,
373
+ summary,
374
+ startedAt: callStartedAt,
375
+ completedAt: new Date().toISOString(),
376
+ });
377
+ if (summary.isError === true) {
378
+ return {
379
+ ok: false,
380
+ missingTools: [],
381
+ calls,
382
+ error: `${nextCall.name} returned an MCP tool error`,
383
+ startedAt,
384
+ completedAt: new Date().toISOString(),
385
+ };
291
386
  }
292
- );
293
- const summary = summarizeToolResult(call.name, result);
294
- calls.push({
295
- name: call.name,
296
- arguments: call.arguments,
297
- ok: summary.isError !== true,
298
- summary,
299
- startedAt: callStartedAt,
300
- completedAt: new Date().toISOString(),
301
- });
302
- if (summary.isError === true) {
387
+
388
+ const isConnectionOnlyEvergreenPrompt =
389
+ nextCall.name === "get_subskill_prompt" &&
390
+ nextCall.arguments?.subskillName === "create-evergreen-campaigns";
391
+ if (!isConnectionOnlyEvergreenPrompt) {
392
+ break;
393
+ }
394
+
395
+ evergreenPromptChunks += 1;
396
+ evergreenPromptOffsets.add(nextCall.arguments?.offset ?? 0);
397
+ mergeConnectionOnlyEvergreenProofs(
398
+ connectionOnlyEvergreenProofs,
399
+ summary.connectionOnlyEvergreenProofs
400
+ );
401
+
402
+ if (summary.hasMore !== true) {
403
+ break;
404
+ }
405
+ if (typeof summary.nextOffset !== "number") {
406
+ return {
407
+ ok: false,
408
+ missingTools: [],
409
+ calls,
410
+ connectionOnlyEvergreenGuidance:
411
+ summarizeConnectionOnlyEvergreenProofs(
412
+ connectionOnlyEvergreenProofs
413
+ ),
414
+ error:
415
+ "create-evergreen-campaigns prompt chunk is missing nextOffset",
416
+ startedAt,
417
+ completedAt: new Date().toISOString(),
418
+ };
419
+ }
420
+ if (evergreenPromptOffsets.has(summary.nextOffset)) {
421
+ return {
422
+ ok: false,
423
+ missingTools: [],
424
+ calls,
425
+ connectionOnlyEvergreenGuidance:
426
+ summarizeConnectionOnlyEvergreenProofs(
427
+ connectionOnlyEvergreenProofs
428
+ ),
429
+ error:
430
+ "create-evergreen-campaigns prompt chunk repeated nextOffset",
431
+ startedAt,
432
+ completedAt: new Date().toISOString(),
433
+ };
434
+ }
435
+ if (
436
+ evergreenPromptChunks >= CONNECTION_ONLY_EVERGREEN_MAX_PROMPT_CHUNKS
437
+ ) {
438
+ return {
439
+ ok: false,
440
+ missingTools: [],
441
+ calls,
442
+ connectionOnlyEvergreenGuidance:
443
+ summarizeConnectionOnlyEvergreenProofs(
444
+ connectionOnlyEvergreenProofs
445
+ ),
446
+ error:
447
+ "create-evergreen-campaigns prompt exceeded smoke chunk limit",
448
+ startedAt,
449
+ completedAt: new Date().toISOString(),
450
+ };
451
+ }
452
+
453
+ nextCall = {
454
+ ...nextCall,
455
+ arguments: {
456
+ ...nextCall.arguments,
457
+ offset: summary.nextOffset,
458
+ limit:
459
+ nextCall.arguments?.limit ||
460
+ CONNECTION_ONLY_EVERGREEN_PROMPT_CHUNK_LIMIT,
461
+ },
462
+ };
463
+ } catch (err) {
464
+ const error = err instanceof Error ? err.message : String(err);
465
+ calls.push({
466
+ name: nextCall.name,
467
+ arguments: nextCall.arguments,
468
+ ok: false,
469
+ summary: null,
470
+ error: redact(error),
471
+ startedAt: callStartedAt,
472
+ completedAt: new Date().toISOString(),
473
+ });
303
474
  return {
304
475
  ok: false,
305
476
  missingTools: [],
306
477
  calls,
307
- error: `${call.name} returned an MCP tool error`,
478
+ error: redact(error),
308
479
  startedAt,
309
480
  completedAt: new Date().toISOString(),
310
481
  };
311
482
  }
312
- } catch (err) {
313
- const error = err instanceof Error ? err.message : String(err);
314
- calls.push({
315
- name: call.name,
316
- arguments: call.arguments,
317
- ok: false,
318
- summary: null,
319
- error: redact(error),
320
- startedAt: callStartedAt,
321
- completedAt: new Date().toISOString(),
322
- });
323
- return {
324
- ok: false,
325
- missingTools: [],
326
- calls,
327
- error: redact(error),
328
- startedAt,
329
- completedAt: new Date().toISOString(),
330
- };
331
483
  }
332
484
  }
333
485
 
486
+ const connectionOnlyEvergreenGuidance =
487
+ summarizeConnectionOnlyEvergreenProofs(connectionOnlyEvergreenProofs);
488
+ if (connectionOnlyEvergreenGuidance.ok !== true) {
489
+ return {
490
+ ok: false,
491
+ missingTools: [],
492
+ calls,
493
+ connectionOnlyEvergreenGuidance,
494
+ error: `create-evergreen-campaigns prompt is missing connection-only guidance: ${connectionOnlyEvergreenGuidance.missing.join(", ")}`,
495
+ startedAt,
496
+ completedAt: new Date().toISOString(),
497
+ };
498
+ }
499
+
334
500
  return {
335
501
  ok: true,
336
502
  missingTools: [],
337
503
  calls,
504
+ connectionOnlyEvergreenGuidance,
338
505
  error: null,
339
506
  startedAt,
340
507
  completedAt: new Date().toISOString(),
341
508
  };
342
509
  }
343
510
 
511
+ function verifyConnectionOnlyEvergreenContract(tools) {
512
+ const setupTool = (tools || []).find(
513
+ (tool) => tool?.name === "setup_evergreen_campaigns"
514
+ );
515
+ const schema = setupTool?.inputSchema || {};
516
+ const properties = schema.properties || {};
517
+ const sequenceOptions = properties.campaignSequenceOptions || {};
518
+ const mode = sequenceOptions.properties?.mode || {};
519
+ const issues = [];
520
+
521
+ if (!properties.workspaceId) {
522
+ issues.push("setup_evergreen_campaigns.workspaceId");
523
+ }
524
+ if (!properties.campaignSequenceOptions) {
525
+ issues.push("setup_evergreen_campaigns.campaignSequenceOptions");
526
+ }
527
+ if (!Array.isArray(mode.enum) || !mode.enum.includes("connection_only")) {
528
+ issues.push("setup_evergreen_campaigns.campaignSequenceOptions.mode");
529
+ }
530
+ if (sequenceOptions.additionalProperties !== false) {
531
+ issues.push(
532
+ "setup_evergreen_campaigns.campaignSequenceOptions.additionalProperties"
533
+ );
534
+ }
535
+
536
+ return {
537
+ ok: issues.length === 0,
538
+ issues,
539
+ };
540
+ }
541
+
344
542
  function summarizeAttempt(result, attempt) {
345
543
  return {
346
544
  attempt,
@@ -348,6 +546,12 @@ function summarizeAttempt(result, attempt) {
348
546
  availableToolCount: result.availableTools?.length || 0,
349
547
  missingToolCount: result.missingTools?.length || 0,
350
548
  forbiddenToolCount: result.forbiddenTools?.length || 0,
549
+ connectionOnlyEvergreenContract: result.connectionOnlyEvergreenContract
550
+ ? {
551
+ ok: result.connectionOnlyEvergreenContract.ok === true,
552
+ issues: result.connectionOnlyEvergreenContract.issues || [],
553
+ }
554
+ : null,
351
555
  error: result.error || null,
352
556
  createCampaignSmoke: result.createCampaignSmoke
353
557
  ? {
@@ -419,6 +623,7 @@ async function verifySellableMcpRuntimeOnce({
419
623
  let missingTools = [...requiredTools];
420
624
  let forbiddenTools = [];
421
625
  let createCampaignSmoke = null;
626
+ let connectionOnlyEvergreenContract = { ok: false, issues: [] };
422
627
  let error = null;
423
628
 
424
629
  try {
@@ -432,6 +637,8 @@ async function verifySellableMcpRuntimeOnce({
432
637
  forbiddenTools = availableTools.filter((tool) =>
433
638
  FORBIDDEN_SELLABLE_MCP_TOOLS.includes(tool)
434
639
  );
640
+ connectionOnlyEvergreenContract =
641
+ verifyConnectionOnlyEvergreenContract(toolList.tools);
435
642
  if (forbiddenTools.length > 0) {
436
643
  createCampaignSmoke = {
437
644
  ok: false,
@@ -475,6 +682,7 @@ async function verifySellableMcpRuntimeOnce({
475
682
  !error &&
476
683
  missingTools.length === 0 &&
477
684
  forbiddenTools.length === 0 &&
685
+ connectionOnlyEvergreenContract.ok === true &&
478
686
  createCampaignSmoke?.ok === true,
479
687
  skipped: false,
480
688
  command,
@@ -483,6 +691,7 @@ async function verifySellableMcpRuntimeOnce({
483
691
  availableTools,
484
692
  missingTools,
485
693
  forbiddenTools,
694
+ connectionOnlyEvergreenContract,
486
695
  createCampaignSmoke,
487
696
  stderrTail: stderrLines,
488
697
  error: error ? redact(error) : null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.316",
3
+ "version": "0.1.318",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {
@@ -721,7 +721,7 @@ Treat host capabilities as concrete functions, not prose conventions:
721
721
  import and before dispatching Message Drafting only.
722
722
  - `launch_message_drafting`: Claude Code uses `Task` with `subagent_type`
723
723
  `post-find-leads-message-scout` when listed; Codex uses the returned
724
- compatibility agent or a generic `gpt-5.5` / `xhigh` Message Drafting agent.
724
+ compatibility agent or a generic `gpt-5.6-sol` / `high` Message Drafting agent.
725
725
 
726
726
  If a required interactive question function or MCP loader is missing, stop and
727
727
  explain the Sellable install/reload problem. Source work uses product-native MCP
@@ -1082,8 +1082,8 @@ updates.
1082
1082
  In Codex, the filter-choice answer is the campaign-scoped go-ahead to use
1083
1083
  this single Message Drafting background agent in step-wise and YOLO modes.
1084
1084
  Do not ask a separate question to start it. If the named custom agent is not
1085
- available, spawn a generic background agent with `model: "gpt-5.5"` and
1086
- `reasoning_effort: "xhigh"` using the same lean campaign/table basis. If no
1085
+ available, spawn a generic background agent with `model: "gpt-5.6-sol"` and
1086
+ `reasoning_effort: "high"` using the same lean campaign/table basis. If no
1087
1087
  background-agent tool is callable, start the same full message branch inline
1088
1088
  before filter drafting or skip-filter message review and record it as
1089
1089
  `statusSource: "parent-thread-fallback"`.
@@ -134,6 +134,19 @@ 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 and persists `currentStep:"send"` as the paused
147
+ review step. It does not require Message Drafting, generated messages,
148
+ route-proof approval, first DM, InMail, View Profile fallback, follow-up
149
+ branch, launch, scheduling, or sends.
137
150
 
138
151
  If the invoking prompt explicitly asks for interactive message polish or sample
139
152
  proof, run in **interactive polish mode** and use the confirmation/sample steps
@@ -424,7 +437,7 @@ fallback only when app thread tools are unavailable; the receipt must include
424
437
  `fallbackReason`, or backend verify rejects it.
425
438
  When visible Codex app thread tools are unavailable but local Codex CLI is
426
439
  available, the accepted durable streaming-worker command shape is:
427
- `codex -a never -s danger-full-access -c model_reasoning_effort=xhigh exec --skip-git-repo-check -m <worker-model> -C <repo> -o <worker-final-file> -`.
440
+ `codex -a never -s danger-full-access -c model_reasoning_effort=high exec --skip-git-repo-check -m <worker-model> -C <repo> -o <worker-final-file> -`.
428
441
  The approval, sandbox, and reasoning-effort config flags must come before the
429
442
  `exec` subcommand for Codex CLI builds that expose `-a`/`-s`/`-c` only at top
430
443
  level. `--skip-git-repo-check` belongs after `exec` because current customer and VPS Codex CLI builds expose it
@@ -434,26 +447,26 @@ forms fail on current customer CLI installs. Pipe the lane packet prompt on
434
447
  stdin, require the worker to write `workerDispatch.receiptArtifactHint`, and
435
448
  pass exactly one lane packet per worker.
436
449
  When launching durable Codex CLI workers from an automation parent, pass an
437
- explicit supported worker model and explicit `xhigh` reasoning effort instead
450
+ explicit supported worker model and explicit `high` reasoning effort instead
438
451
  of relying on the Codex CLI defaults. Use the parent runtime model when known,
439
452
  for example:
440
- `codex -a never -s danger-full-access -c model_reasoning_effort=xhigh exec --skip-git-repo-check -m <worker-model> -C <repo> -o <worker-final-file> -`.
453
+ `codex -a never -s danger-full-access -c model_reasoning_effort=high exec --skip-git-repo-check -m <worker-model> -C <repo> -o <worker-final-file> -`.
441
454
  In Codex CLI, the parent runtime model is visible in the run header as
442
455
  `model: <model-name>`. Copy that exact model string into child worker launches
443
- first. If the parent header says `model: gpt-5.5`, launch workers with
444
- `-m gpt-5.5` plus `-c model_reasoning_effort=xhigh`; do not invent or probe
456
+ first. If the parent header says `model: gpt-5.6-sol`, launch workers with
457
+ `-m gpt-5.6-sol` plus `-c model_reasoning_effort=high`; do not invent or probe
445
458
  nearby aliases such as `gpt-5.3-codex`, `gpt-5.2`, `gpt-5-codex`,
446
459
  `codex-latest`, or `codex-mini-latest` before trying the exact parent model. A
447
- one-line probe must include the same `-c model_reasoning_effort=xhigh` override
460
+ one-line probe must include the same `-c model_reasoning_effort=high` override
448
461
  and count as supported only when it exits 0 and returns the requested output; a
449
462
  session header followed by a `not supported` error is rejected, not accepted.
450
463
  Do not rely on the Codex CLI default model or default reasoning effort; some
451
- customer and VPS installs default to unavailable model aliases or to `high`
452
- reasoning. `-m gpt-5.5` alone is not enough. If a child worker reports GPT 5.5
453
- with `high` reasoning, treat that as a launcher bug, relaunch with the explicit
454
- `xhigh` config before mutation, and do not ask the user to continue through the
464
+ customer and VPS installs default to unavailable model aliases or to reasoning
465
+ below `high`. `-m gpt-5.6-sol` alone is not enough. If a child worker reports GPT 5.6-Sol
466
+ with reasoning below `high`, treat that as a launcher bug, relaunch with the explicit
467
+ `high` config before mutation, and do not ask the user to continue through the
455
468
  model-quality warning. If the parent cannot identify a supported worker model
456
- and launch it with `xhigh` reasoning, stop with
469
+ and launch it with `high` reasoning, stop with
457
470
  `blocked: worker_model_unavailable` before mutation.
458
471
  When wrapping multiple local Codex CLI workers in a shell launcher, run the
459
472
  wrapper with `/bin/bash -lc` or another explicitly chosen portable shell. Do not
@@ -466,7 +479,7 @@ Do not embed `<<'WORKER_PROMPT'` heredocs inside a single quoted or double
466
479
  quoted `/bin/bash -lc '...'` command string; nested quoting is brittle and can
467
480
  truncate the first worker before mutation. For multi-worker launchers, write one
468
481
  plain prompt file per lane under the current run directory, then start each
469
- worker with `codex -a never -s danger-full-access -c model_reasoning_effort=xhigh exec --skip-git-repo-check -m "$WORKER_MODEL" -C "$REPO" -o
482
+ worker with `codex -a never -s danger-full-access -c model_reasoning_effort=high exec --skip-git-repo-check -m "$WORKER_MODEL" -C "$REPO" -o
470
483
  "$worker_final_file" - < "$worker_prompt_file"`. Keep launcher shell variables
471
484
  double-quoted and keep the prompt heredoc only in a standalone script/prompt-file
472
485
  write step, not inside an already quoted shell argument. If the first launcher
@@ -572,7 +585,7 @@ launchers, write the same prompt body to `<worker-prompt-file>` and launch the
572
585
  worker with stdin redirected from that file:
573
586
 
574
587
  ```
575
- codex -a never -s danger-full-access -c model_reasoning_effort=xhigh exec --skip-git-repo-check -m <worker-model> -C <repo> -o <worker-final-file> - <<'WORKER_PROMPT'
588
+ codex -a never -s danger-full-access -c model_reasoning_effort=high exec --skip-git-repo-check -m <worker-model> -C <repo> -o <worker-final-file> - <<'WORKER_PROMPT'
576
589
  Use $sellable:create-campaign as the governing campaign workflow for this one lane worker.
577
590
  Use the evergreen plan/packet below only for lane scope, source metadata,
578
591
  postconditions, side-effect caps, and durable receipt proof.
@@ -619,10 +632,10 @@ Do not launch, start, schedule, send, or use paid InMail.
619
632
  For filter proof, durable receipt status must be `filterDecisionReceipt.status:"applied"` only; never `completed`, `confirmed`, `done`, or aliases.
620
633
  For generated messages, `update_cell` is allowed only for the semantic Approved checkbox. Never use `update_cell` for generated message text/body/sample copy. Bad copy requires `revise_message_template_and_rerun` or brief/template revision plus Generate Message rerun. Any generated-message cell override is `blocked: generated_message_cell_override`.
621
634
  If `bootstrap_create_campaign.modelQuality.status === "warn"` because the child
622
- worker reports GPT 5.5 with `high` reasoning, that is a parent launcher
635
+ worker reports GPT 5.6-Sol with reasoning below `high`, that is a parent launcher
623
636
  configuration bug, not an operator approval path inside the worker. Stop before
624
637
  mutation, tell the parent to relaunch this lane with
625
- `-c model_reasoning_effort=xhigh`, and do not mark the worker goal complete.
638
+ `-c model_reasoning_effort=high`, and do not mark the worker goal complete.
626
639
  Complete only this lane. Do not end with narration only. Before your final
627
640
  response, run a local file-existence and JSON self-check for
628
641
  <receiptArtifactPath>. If the lane succeeded, write the canonical success
@@ -643,7 +656,7 @@ receipt, or an accepted Post Engagers no-source blocked/no-op receipt.
643
656
  WORKER_PROMPT
644
657
 
645
658
  # Multi-worker launcher equivalent:
646
- codex -a never -s danger-full-access -c model_reasoning_effort=xhigh exec --skip-git-repo-check -m <worker-model> -C <repo> -o <worker-final-file> - < <worker-prompt-file>
659
+ codex -a never -s danger-full-access -c model_reasoning_effort=high exec --skip-git-repo-check -m <worker-model> -C <repo> -o <worker-final-file> - < <worker-prompt-file>
647
660
  ```
648
661
 
649
662
  If any placeholder cannot be filled from the current plan, matching
@@ -720,6 +733,9 @@ not a parent-thread summary. The receipt must include:
720
733
  creation, preserve that original packet only in a separate `parentLanePacket`
721
734
  object; do not leave `setupPlanCall.campaignId:null` or
722
735
  `setupPlanCall.tableId:null`.
736
+ If the parent plan used connection-only, the receipt must also include
737
+ `setupPlanCall.campaignSequenceOptions:{ "mode":"connection_only" }` or
738
+ `setupPlanCall.sequencePolicy.mode:"connection_only"`.
723
739
  Short form: For create lanes, `setupPlanCall.campaignId` and `setupPlanCall.tableId` must be the actual created campaign/table ids.
724
740
  Short form: Do not leave `setupPlanCall.campaignId:null` or `setupPlanCall.tableId:null`.
725
741
  - `createCampaignWorkflowReceipt`: proof the worker loaded the actual installed
@@ -810,22 +826,40 @@ not a parent-thread summary. The receipt must include:
810
826
  them into canonical `promptLoadedToHasMoreFalse`, `requiredAssetsLoaded`,
811
827
  `validationLoaded`, `reviewBatchRowHash`, and
812
828
  `messageDraftRecommendation`.
829
+ Connection-only lanes are the exception: do not include
830
+ `messageDraftingReceipt` or run generated-message prep unless the operator
831
+ separately asked for message copy. The parent verifier expects no message
832
+ proof for connection-only lanes.
813
833
  - `reviewBatchReceipt`: review-batch row ids/hash, generated row count,
814
834
  quality-valid route-proof row id when approved, and proof that no broad
815
835
  approve-all occurred.
836
+ Connection-only lanes are the exception: do not approve a route-proof row and
837
+ do not include a required review-batch receipt.
816
838
  - `sequenceReceipt`: exact current workflowTableId, recommended non-paid
817
839
  sequence attach/precheck result, and readback showing `hasSequence:true` when
818
840
  completion is claimed. If the tool output uses
819
841
  `attachRecommendedSequenceResult.actionTypes` or
820
842
  `nonPaidRecommendedSequence`, copy them to canonical `actionTypes` and
821
843
  `nonPaid`.
844
+ For connection-only lanes, do not call `attach_recommended_sequence`. Attach
845
+ with `attach_sequence({ tableId, templateRef:"connection_only", currentStep:"send" })`, then
846
+ include exact current-template proof:
847
+ `sequenceReceipt.hasSequence:true`,
848
+ `sequenceReceipt.actionTypes:["send_invite"]`, and
849
+ `sequenceReceipt.nonPaid:true`. Receipts that only say
850
+ `tool:"attach_recommended_sequence"` or include `send_dm`,
851
+ `send_inmail_open`, `send_inmail_closed`, or `view_profile` are failing
852
+ connection-only receipts.
822
853
  - final paused-send proof: if the current campaign table is `DRAFT` after the
823
854
  sequence is attached, call the product `pause_campaign({ campaignId })`
824
855
  endpoint/tool to put the unlaunched campaign into `PAUSED` review state, then
825
856
  reread the campaign/table. Do not raw-write `campaignStatus`, do not start or
826
857
  launch, and do not schedule/send. Completion requires reread proof of
827
- `currentStep:"send"` and `campaignStatus:"PAUSED"`. If the raw output nests
828
- this in `finalCampaignRead` or `finalTableRead`, copy it to canonical
858
+ `currentStep:"send"` and `campaignStatus:"PAUSED"` for standard and
859
+ connection-only customer-visible lanes. `send` is the product's paused
860
+ review step; do not use unsupported aliases such as `review` as final proof.
861
+ If the raw output nests this in `finalCampaignRead` or `finalTableRead`, copy
862
+ it to canonical
829
863
  `finalPausedSendProof.currentStep` and
830
864
  `finalPausedSendProof.campaignStatus`.
831
865
  - `verifyCall`: the exact `setup_evergreen_campaigns({ mode:"verify",
@@ -1088,9 +1122,14 @@ plain row generation. The lane worker must inline the same
1088
1122
  approvedGeneratedMessageCount exactly 1. Do not report completion with 2+
1089
1123
  approved rows.
1090
1124
  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.
1125
+ 8. Standard mode: continue to
1126
+ `attach_recommended_sequence({ campaignId, currentStep:"send" })` and, if
1127
+ the campaign is still `DRAFT`, `pause_campaign({ campaignId })`. Reread the
1128
+ campaign/table before claiming completion.
1129
+ Connection-only mode: skip Message Drafting and route-proof approval, call
1130
+ `attach_sequence({ tableId, templateRef:"connection_only", currentStep:"send" })`, prove
1131
+ `sequenceReceipt.actionTypes:["send_invite"]`, and pause/reread without
1132
+ launching or sending.
1094
1133
 
1095
1134
  That packaged worker path must return `messageDraftingReceipt.statusSource:
1096
1135
  "packaged-generate-messages-worker"`. It is accepted only when the receipt
@@ -1515,11 +1554,14 @@ Message, and verify current-revision sample messages before final completion.
1515
1554
  - 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
1555
  - **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
1556
 
1518
- - The sequence is auto-selected by sender tier; do not hand-author sequence
1557
+ - In standard mode, the sequence is auto-selected by sender tier; do not hand-author sequence
1519
1558
  templates here. Sales Nav/Recruiter senders may receive the unified Sales
1520
1559
  Nav cascade through `attach_recommended_sequence`; attaching it does not
1521
1560
  spend paid InMail credits by itself. Do not substitute the manual Paid
1522
1561
  InMail Campaign template.
1562
+ For explicit connection-only evergreen lanes only, use the backend-owned
1563
+ `attach_sequence({ tableId, templateRef:"connection_only", currentStep:"send" })` path and do
1564
+ not call `attach_recommended_sequence`.
1523
1565
  3. **Customer-Visible Completion Contract**: a named evergreen lane that appears
1524
1566
  as a campaign card or campaign-backed table is not done when the shell exists.
1525
1567
  It is done only when the customer can open the campaign and land on final
@@ -1543,7 +1585,7 @@ Message, and verify current-revision sample messages before final completion.
1543
1585
  evergreen setup. Do not leave customer-visible campaigns at
1544
1586
  `filter-choice`, `filter-rules`, or `apply-icp-rubric` and report success.
1545
1587
  Do not proceed to Message Drafting until saved filters are applied.
1546
- - Message Drafting has run from the current campaign/table basis, using the
1588
+ - In standard mode, Message Drafting has run from the current campaign/table basis, using the
1547
1589
  create-campaign message prompt/assets and validation gate. Updating
1548
1590
  `currentStep:"messages"` is not proof. Parent-thread handwritten copy is
1549
1591
  not a substitute. The proof receipt must show
@@ -1584,6 +1626,9 @@ Message, and verify current-revision sample messages before final completion.
1584
1626
  Fallback samples must still reject source/conversation hedges such as
1585
1627
  `"hope this is relevant"`, `"might be interested"`, or `"saw you in a few
1586
1628
  conversations"`.
1629
+ For explicit connection-only lanes, this whole Message Drafting and sample
1630
+ proof block is not required and must not be faked with parent-written
1631
+ messages.
1587
1632
  - The first review batch exists and at least 3 review rows have generated
1588
1633
  messages from the approved brief. If fewer than 3 usable rows exist, report
1589
1634
  the actual count and why.
@@ -1596,7 +1641,7 @@ Message, and verify current-revision sample messages before final completion.
1596
1641
  completion, approve exactly one quality-valid generated row. If one or more
1597
1642
  rows are already approved, do not add more approvals during evergreen
1598
1643
  completion. Never broad approve all rows.
1599
- - The recommended tier-aware sequence is attached to the current campaign
1644
+ - In standard mode, the recommended tier-aware sequence is attached to the current campaign
1600
1645
  table, and the watched campaign is on Send. Use
1601
1646
  `attach_recommended_sequence({ campaignId, currentStep:"send" })` when a
1602
1647
  safe attach is needed. After `confirm_lead_list` or any source-list copy,
@@ -1611,6 +1656,10 @@ Message, and verify current-revision sample messages before final completion.
1611
1656
  manual Paid InMail Campaign, and never attach/replace sequence outside the
1612
1657
  current table unless the current lane packet explicitly allowed sequence
1613
1658
  repair.
1659
+ For explicit connection-only lanes, the current campaign table must have
1660
+ exactly the canonical invite-only sequence:
1661
+ `sequenceReceipt.actionTypes:["send_invite"]`. Any DM, InMail, or View
1662
+ Profile action means the lane is not complete.
1614
1663
  - If the current campaign table is still `DRAFT` after sequence/readiness
1615
1664
  proof, call `pause_campaign({ campaignId })` and reread. `pause_campaign`
1616
1665
  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