@sellable/install 0.1.752 → 0.1.754

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.
Files changed (18) hide show
  1. package/container/Dockerfile +4 -4
  2. package/container/README.md +1 -1
  3. package/lib/sellable-agent/default-profile-bundle.mjs +37 -14
  4. package/lib/sellable-agent/default-profile-bundles/shared/manifest.fragment.json +6 -6
  5. package/lib/sellable-agent/default-profile-reconciler.mjs +86 -17
  6. package/lib/sellable-agent/fly-admin-image/Dockerfile +2 -2
  7. package/lib/sellable-agent/fly-customer-image/Dockerfile +3 -3
  8. package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +1 -1
  9. package/lib/sellable-agent/host-bootstrap.mjs +1 -1
  10. package/lib/sellable-agent/host-worker.mjs +1 -1
  11. package/lib/sellable-agent/profile-materializer.mjs +1 -1
  12. package/lib/sellable-agent/provisioning-adapter.mjs +1 -1
  13. package/package.json +1 -1
  14. /package/lib/sellable-agent/default-profile-bundles/shared/skills/{sellable-defaults/campaign-daily-results → sellable/sellable-campaign-daily-results}/SKILL.md +0 -0
  15. /package/lib/sellable-agent/default-profile-bundles/shared/skills/{sellable-defaults/campaign-daily-review → sellable/sellable-campaign-daily-review}/SKILL.md +0 -0
  16. /package/lib/sellable-agent/default-profile-bundles/shared/skills/{sellable-defaults/campaign-weekly-review → sellable/sellable-campaign-weekly-review}/SKILL.md +0 -0
  17. /package/lib/sellable-agent/default-profile-bundles/shared/skills/{sellable-defaults/create-campaign → sellable/sellable-create-campaign}/SKILL.md +0 -0
  18. /package/lib/sellable-agent/default-profile-bundles/shared/skills/{sellable-defaults/refill-sends → sellable/sellable-refill-sends}/SKILL.md +0 -0
@@ -3,12 +3,12 @@ FROM ${HERMES_IMAGE}
3
3
 
4
4
  USER root
5
5
 
6
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.751
6
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.754
7
7
  ARG MCP_PACKAGE=@sellable/mcp@0.1.938
8
8
  ARG INSTALLER_INTEGRITY
9
9
  ARG MCP_INTEGRITY=sha512-wcVCiCXVvKeE/SufP+2iXk8zgzwYWh78M/1vwa7oorF9UZbzut9szyZV47t4iMX9owYgBVaGwdlOC6eX8VQX4A==
10
10
 
11
- RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.751" \
11
+ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.754" \
12
12
  && test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.938" \
13
13
  && test -n "${INSTALLER_INTEGRITY}" \
14
14
  && test "$(npm view "${INSTALLER_PACKAGE}" dist.integrity)" = "${INSTALLER_INTEGRITY}" \
@@ -23,9 +23,9 @@ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.751" \
23
23
  && mkdir -p /opt/sellable-agent/install-root /opt/sellable-agent/mcp-root \
24
24
  && npm install --prefix /opt/sellable-agent/install-root --omit=dev --ignore-scripts --no-audit --no-fund "${INSTALLER_PACKAGE}" \
25
25
  && npm install --prefix /opt/sellable-agent/mcp-root --omit=dev --ignore-scripts --no-audit --no-fund "${MCP_PACKAGE}" \
26
- && node --input-type=module -e "import { readFileSync } from 'node:fs'; const install = JSON.parse(readFileSync('/opt/sellable-agent/install-root/node_modules/@sellable/install/package.json')); const mcp = JSON.parse(readFileSync('/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp/package.json')); if (install.name !== '@sellable/install' || install.version !== '0.1.751' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.938') throw new Error('package identity rejected');" \
26
+ && node --input-type=module -e "import { readFileSync } from 'node:fs'; const install = JSON.parse(readFileSync('/opt/sellable-agent/install-root/node_modules/@sellable/install/package.json')); const mcp = JSON.parse(readFileSync('/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp/package.json')); if (install.name !== '@sellable/install' || install.version !== '0.1.754' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.938') throw new Error('package identity rejected');" \
27
27
  && node --input-type=module -e "import { buildExternalRuntimeClosure } from '/opt/sellable-agent/install-root/node_modules/@sellable/install/lib/sellable-agent/external-runtime-builder.mjs'; const built = buildExternalRuntimeClosure({ mcpPackageRoot: '/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp', mcpNodeModulesRoot: '/opt/sellable-agent/mcp-root/node_modules', hermesSourceRoot: '/opt/hermes', ownerUid: 0, ownerGid: 0 }); if (!built.closureDigest) throw new Error('runtime closure rejected');" \
28
- && node --input-type=module -e "import { chmodSync, writeFileSync } from 'node:fs'; const release = { installerPackage: '@sellable/install@0.1.751', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.938', mcpIntegrity: process.argv[2] }; writeFileSync('/opt/sellable-agent/release.json', JSON.stringify(release) + '\\n', { mode: 0o444 }); chmodSync('/opt/sellable-agent/release.json', 0o444);" "${INSTALLER_INTEGRITY}" "${MCP_INTEGRITY}" \
28
+ && node --input-type=module -e "import { chmodSync, writeFileSync } from 'node:fs'; const release = { installerPackage: '@sellable/install@0.1.754', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.938', mcpIntegrity: process.argv[2] }; writeFileSync('/opt/sellable-agent/release.json', JSON.stringify(release) + '\\n', { mode: 0o444 }); chmodSync('/opt/sellable-agent/release.json', 0o444);" "${INSTALLER_INTEGRITY}" "${MCP_INTEGRITY}" \
29
29
  && npm cache clean --force
30
30
 
31
31
  COPY entrypoint.sh /usr/local/bin/sellable-agent-container-entrypoint
@@ -3,7 +3,7 @@
3
3
  This image is a dedicated Agent worker/runtime boundary beside an existing Hermes dashboard. It never replaces or reconfigures the dashboard container.
4
4
 
5
5
  Build from this directory after
6
- `@sellable/install@0.1.751` is
6
+ `@sellable/install@0.1.754` is
7
7
  published:
8
8
 
9
9
  ```sh
@@ -20,7 +20,10 @@ const FRAGMENTS = join(HERE, "default-profile-bundles");
20
20
  const SCHEMA = "sellable-agent-default-profile-fragment/v1";
21
21
  const BUNDLE_SCHEMA = "sellable-agent-default-profile-bundle/v1";
22
22
  const ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
23
- const SKILL_PATH = /^skills\/sellable-defaults\/([a-z0-9][a-z0-9-]{0,63})(?:\/.*)?$/;
23
+ const SKILL_PATH =
24
+ /^skills\/sellable-defaults\/([a-z0-9][a-z0-9-]{0,63})(?:\/.*)?$/;
25
+ const PUBLIC_SKILL_PATH =
26
+ /^skills\/sellable\/([a-z0-9][a-z0-9-]{0,127})\/SKILL\.md$/;
24
27
  const CRON_PATH = /^crons\/([a-z0-9][a-z0-9-]{0,63})\.json$/;
25
28
  const MAX_FILES = 64;
26
29
  const MAX_FILE_BYTES = 64 * 1024;
@@ -78,7 +81,10 @@ function assertClosedFragmentRoot(path, fragment) {
78
81
  if (entry.isSymbolicLink()) reject("symlink_rejected");
79
82
  if (entry.isDirectory()) {
80
83
  for (const name of readdirSync(candidate).sort()) {
81
- visit(join(candidate, name), relativePath ? `${relativePath}/${name}` : name);
84
+ visit(
85
+ join(candidate, name),
86
+ relativePath ? `${relativePath}/${name}` : name
87
+ );
82
88
  }
83
89
  return;
84
90
  }
@@ -88,7 +94,11 @@ function assertClosedFragmentRoot(path, fragment) {
88
94
  fragment.fragmentId === "shared" &&
89
95
  SHARED_FRAGMENT_AUXILIARY_FILES.has(relativePath)
90
96
  ) {
91
- if ((entry.mode & 0o111) === 0 || entry.size < 1 || entry.size > MAX_FILE_BYTES) {
97
+ if (
98
+ (entry.mode & 0o111) === 0 ||
99
+ entry.size < 1 ||
100
+ entry.size > MAX_FILE_BYTES
101
+ ) {
92
102
  reject("auxiliary_file_rejected");
93
103
  }
94
104
  return;
@@ -114,7 +124,10 @@ function readFragment(path) {
114
124
  if (bytes.byteLength > MAX_FRAGMENT_BYTES) reject("fragment_too_large");
115
125
  value = JSON.parse(bytes.toString("utf8"));
116
126
  } catch (error) {
117
- if (error instanceof Error && error.message.startsWith("default_profile_bundle_")) {
127
+ if (
128
+ error instanceof Error &&
129
+ error.message.startsWith("default_profile_bundle_")
130
+ ) {
118
131
  throw error;
119
132
  }
120
133
  reject("fragment_invalid");
@@ -193,9 +206,16 @@ function normalizeFile(value, fragmentKind) {
193
206
  ) {
194
207
  reject("content_rejected");
195
208
  }
209
+ let nativeName = null;
196
210
  if (value.type === "skill") {
197
- const match = SKILL_PATH.exec(value.path);
198
- if (!match || match[1] !== value.id || !value.path.endsWith("/SKILL.md")) {
211
+ nativeName = readNativeSkillName(value.content);
212
+ const managedMatch = SKILL_PATH.exec(value.path);
213
+ const publicMatch = PUBLIC_SKILL_PATH.exec(value.path);
214
+ const managedPathValid =
215
+ managedMatch?.[1] === value.id && value.path.endsWith("/SKILL.md");
216
+ const canonicalPublicPathValid =
217
+ publicMatch?.[1] === nativeName && nativeName === `sellable-${value.id}`;
218
+ if (!managedPathValid && !canonicalPublicPathValid) {
199
219
  reject("skill_path_rejected");
200
220
  }
201
221
  } else {
@@ -215,9 +235,7 @@ function normalizeFile(value, fragmentKind) {
215
235
  kind: fragmentKind,
216
236
  path: value.path,
217
237
  mode: value.mode,
218
- ...(value.type === "cron"
219
- ? { name: value.name }
220
- : { nativeName: readNativeSkillName(value.content) }),
238
+ ...(value.type === "cron" ? { name: value.name } : { nativeName }),
221
239
  content: value.content,
222
240
  sha256: sha256(bytes),
223
241
  });
@@ -297,15 +315,17 @@ export function buildDefaultProfileBundle({
297
315
  files.reduce((sum, file) => sum + Buffer.byteLength(file.content), 0) >
298
316
  MAX_TOTAL_BYTES ||
299
317
  new Set(files.map((file) => file.path)).size !== files.length ||
300
- new Set(files.filter((file) => file.type === "skill").map((file) => file.id))
301
- .size !== files.filter((file) => file.type === "skill").length ||
318
+ new Set(
319
+ files.filter((file) => file.type === "skill").map((file) => file.id)
320
+ ).size !== files.filter((file) => file.type === "skill").length ||
302
321
  new Set(
303
322
  files
304
323
  .filter((file) => file.type === "skill")
305
324
  .map((file) => file.nativeName)
306
325
  ).size !== files.filter((file) => file.type === "skill").length ||
307
- new Set(files.filter((file) => file.type === "cron").map((file) => file.name))
308
- .size !== files.filter((file) => file.type === "cron").length
326
+ new Set(
327
+ files.filter((file) => file.type === "cron").map((file) => file.name)
328
+ ).size !== files.filter((file) => file.type === "cron").length
309
329
  ) {
310
330
  reject("entry_collision");
311
331
  }
@@ -351,7 +371,10 @@ function argumentValue(flag) {
351
371
  return index >= 0 ? process.argv[index + 1] : undefined;
352
372
  }
353
373
 
354
- if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
374
+ if (
375
+ process.argv[1] &&
376
+ resolve(process.argv[1]) === fileURLToPath(import.meta.url)
377
+ ) {
355
378
  const result = buildDefaultProfileBundle({
356
379
  kind: argumentValue("--kind"),
357
380
  adminFragmentPath: argumentValue("--admin-fragment"),
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": "sellable-agent-default-profile-fragment/v1",
3
3
  "fragmentId": "shared",
4
4
  "kind": "SHARED",
5
- "bundleVersion": 3,
5
+ "bundleVersion": 4,
6
6
  "materializerReceipts": [
7
7
  "SOUL.md"
8
8
  ],
@@ -11,7 +11,7 @@
11
11
  "id": "create-campaign",
12
12
  "type": "skill",
13
13
  "ownership": "system_managed",
14
- "path": "skills/sellable-defaults/create-campaign/SKILL.md",
14
+ "path": "skills/sellable/sellable-create-campaign/SKILL.md",
15
15
  "mode": "0444",
16
16
  "content": "---\nname: sellable-create-campaign\ndescription: Create, continue, or edit a Sellable campaign through the shell-first CampaignOffer workflow.\nvisibility: public\nallowed-tools:\n - mcp_sellable_get_auth_status\n - mcp_sellable_customer_program\n - mcp_sellable_start_cli_login\n - mcp_sellable_wait_for_cli_login\n - mcp_sellable_validate_campaign1_kickoff_handoff\n - mcp_sellable_bootstrap_create_campaign\n - mcp_sellable_get_subskill_prompt\n - mcp_sellable_get_subskill_asset\n - mcp_sellable_search_subskill_prompts\n - mcp_sellable_get_provider_prompt\n - mcp_sellable_get_source_scout_registry\n - mcp_sellable_get_post_find_leads_scout_registry\n - mcp_sellable_get_active_workspace\n - mcp_sellable_list_workspaces\n - mcp_sellable_set_active_workspace\n - mcp_sellable_list_senders\n - mcp_sellable_get_sender\n - mcp_sellable_complete_sender_research\n - mcp_sellable_fetch_linkedin_profile\n - mcp_sellable_fetch_linkedin_posts\n - mcp_sellable_get_linkedin_profile\n - mcp_sellable_fetch_company\n - mcp_sellable_fetch_company_posts\n - mcp_sellable_lookup_sales_nav_filter\n - mcp_sellable_search_sales_nav\n - mcp_sellable_search_prospeo\n - mcp_sellable_search_prospeo_companies\n - mcp_sellable_confirm_prospeo_company_accounts\n - mcp_sellable_search_harvest_jobs\n - mcp_sellable_confirm_harvest_job_companies\n - mcp_sellable_search_signals\n - mcp_sellable_fetch_post_engagers\n - mcp_sellable_enrich_with_prospeo\n - mcp_sellable_bulk_enrich_with_prospeo\n - mcp_sellable_save_domain_filters\n - mcp_sellable_add_rubric_item\n - mcp_sellable_upsert_rubric\n - mcp_sellable_set_headline_icp_criteria\n - mcp_sellable_check_rubric\n - mcp_sellable_create_campaign\n - mcp_sellable_save_rubrics\n - mcp_sellable_get_campaign_table_schema\n - mcp_sellable_select_campaign_cells\n - mcp_sellable_record_campaign_review_batch\n - mcp_sellable_queue_campaign_cells\n - mcp_sellable_wait_for_campaign_processing\n - mcp_sellable_resolve_campaign_fill_route\n - mcp_sellable_get_campaign_refill_state\n - mcp_sellable_get_refill_target_plan\n - mcp_sellable_fill_campaign_horizon\n - mcp_sellable_start_campaign_message_preparation\n - mcp_sellable_get_campaign_message_preparation_status\n - mcp_sellable_cancel_campaign_message_preparation\n - mcp_sellable_revise_message_template_and_rerun\n - mcp_sellable_update_campaign_brief\n - mcp_sellable_update_campaign\n - mcp_sellable_get_campaign\n - mcp_sellable_get_campaign_context\n - mcp_sellable_get_campaign_framework\n - mcp_sellable_get_campaign_navigation_state\n - mcp_sellable_confirm_lead_list\n - mcp_sellable_import_leads\n - mcp_sellable_wait_for_lead_list_ready\n - mcp_sellable_wait_for_campaign_table_ready\n - mcp_sellable_get_rows\n - mcp_sellable_get_rows_minimal\n - mcp_sellable_get_table_rows\n - mcp_sellable_list_dnc_entries\n - mcp_sellable_load_csv_dnc_entries\n - mcp_sellable_load_csv_linkedin_leads\n - mcp_sellable_load_csv_domains\n - mcp_sellable_get_campaign_messages_preview\n - mcp_sellable_attach_sequence\n - mcp_sellable_attach_recommended_sequence\n - mcp_sellable_start_campaign\n - mcp_sellable_add_to_inmail_campaign\n - mcp_sellable_add_to_connection_campaign\n - mcp_sellable_get_or_create_direct_campaign_table\n - mcp_sellable_start_direct_campaign\n---\n\n# Sellable Create Campaign\n\n## Installed Host Contract\n\nThis installed skill is running in Hermes Agent. When the shared workflow body\nor fallback text mentions Claude Code, Codex, or Hermes for internal parity,\nchoose the Hermes instruction for customer-facing language and host functions.\n\n- Customer-facing command: `/sellable-create-campaign`\n- MCP tool naming: Hermes exposes Sellable tools as `mcp_sellable_<tool>`;\n when shared instructions show `mcp_sellable_<tool>`, call the matching\n `mcp_sellable_<tool>` tool instead.\n- Structured questions: ask plainly in chat unless a Hermes-native approval or\n question tool is visible in the current session.\n- Bootstrap host label: `host: \"Hermes\"`\n- Install/reload blocker label: Hermes install/reload problem\n- Reload instruction: restart Hermes, or run `/reload-mcp` in the active\n Hermes session after install\n\nDo not tell Hermes users to run Codex or Claude command forms, use Codex/Claude structured-question APIs, or restart Codex Desktop or Claude Code. Do not describe this run as Claude Code or Codex.\n\nUse this as the customer-facing public wrapper for Sellable campaign creation.\nIt bootstraps auth and host capabilities, then loads the internal\nthe internal campaign workflow prompt and `core/flow.v2.json` through MCP. Keep\nthis wrapper thin; the packaged workflow is the operational source of truth.\n\n## Direct Manual-Copy Campaign Routing\n\nRoute a bounded one-off request to the direct campaign tools when the user\nalready supplies the recipients and exact message copy. For InMail, require a\nLinkedIn URL, Subject, and Message per row, select the intended sender, then use\n`add_to_inmail_campaign`. For a connection-plus-DM request, use\n`add_to_connection_campaign`. These tools create or reuse the canonical typed\nexecution-queue table; do not call `create_campaign`,\n`create_on_demand_campaign`, or a connection helper first.\n\nDo not create a normal Campaign Builder Generate Message column, rewrite the\napproved manual copy, or repair the typed direct table merely because its\nmessage is stored in a plain `Message` text column. That is the owned schema for\n`inmail_campaign` and `connection_campaign` tables.\n\nShow the bounded recipients and exact copy for review before adding rows. Never\ncall `start_direct_campaign` until the user explicitly approves launch. When\napproved, start the same direct type used by the add tool; do not call\n`start_campaign` for that table.\n\nBilling diagnostics are not launch authority. Honor a billing error returned by\nthe actual add/start mutation, but never interpret `billing_required`\ndiagnostics as an enforced block when the billing rollout gate is disabled.\n\n## Inbox Reply Tool Boundary\n\nInbox reply tools are outside campaign creation. If a user asks to search,\nreview, edit, or send inbox replies, treat that as a separate inbox workflow\nusing the existing product /api/v3/inbox routes. Any draft edit or send requires\nexact approval for the workspace, sender, thread, body, tool, and expected side\neffect before the write tool is called.\n\nCampaignOffer state and the watch link are the customer-facing source of truth.\nDisk artifacts are optional debug/UAT diagnostics; normal customer runs should\nnot create, link, or surface local draft files unless the user explicitly asks\nfor them. Resume, gating, and handoff read campaign state first. The\nwatchable campaign exists after the short brief; source import copies the\nconfirmed list into the campaign, then an internal first campaign-table\nexecution slice configures filters and messages. After that, the user chooses\nwhether to use filters or skip.\nWhen filters are chosen, save rubrics, get filter approval, then wait for\nmessage-template approval before enrichment/filtering or Generate Message cells.\nFilter work stays in the parent thread and must inspect visible campaign rows\nwith `get_rows_minimal` / `get_rows` before drafting and saving rubrics; use at\nmost one direct `enrich_with_prospeo` sample when row evidence is too thin.\nAfter filter approval, the browser should move to Filter Leads with\n`currentStep: \"apply-icp-rubric\"` and show template waiting/approval copy until\nthe template is approved.\nIf the bounded filter run later returns `0/N` passes, do not immediately find\nnew leads. Load `references/sample-validation-loop.md`, run the zero-pass\nrule-relaxability audit, then choose exactly one recovery: revise saved\nrubrics, record a fresh same-source review batch, or change lead source. Change\nsource only when safe relaxation and same-source sampling would still fail or\nwould pass bad-fit rows.\nThe default path stays the existing first campaign-table execution slice:\nreview the normal `reviewBatchLimit:15`, approve reviewed draft rows, then move\nto Settings/sequence/final greenlight. For any plain post-mint fill request\nsuch as \"fill campaigns\", \"fill up\", \"refill sends\", \"max out sends\", or\n\"load sends\", hand off to the skill-led refill workflow:\n\n```text\nget_subskill_prompt({ subskillName: \"refill-sends-workflow\" })\nresolve_campaign_fill_route({ intent:\"plain\" })\nlist_senders\nget_campaign_refill_state\n```\n\nPlain fill is not an alias for `fill_campaign_horizon` or campaign creation.\n`fill_campaign_horizon` is evergreen-only and must not be used for regular\ncampaign refill. Route outcomes are `route:\"evergreen_horizon\"`,\n`route:\"active_campaigns\"`, and `route:\"ask_create\"`; stay in the same `campaignOfferId`/`campaignId` context after minting. If route resolution\nreturns `route:\"ask_create\"`, ask whether to create a normal campaign or\nevergreen campaigns. Campaign creation is allowed only after this route and\nexplicit user selection; it is never the default response to plain fill. When\nchoosing among active targets, use refill-state scheduled-count evidence to\ncontinue the campaign that most recently had scheduler-owned sends for the\nrelevant sender set; do not treat every active campaign as a refill candidate.\nIn `--yolo` fill/refill mode, default to one best recent-send campaign and the\nscheduler-forward 48-hour target window: subtract future scheduler-owned\nscheduled sends and ready-to-schedule rows from healthy sender daily capacity,\nthen prepare only that bounded gap.\nWhen more leads are needed, the refill workflow must recommend same-campaign\nsource-ladder replenishment first; do not create warm-post-engager side\ncampaigns, on-demand campaigns, or unrelated campaigns. If same-source copy hits\n`USER_ADDED_ROWS_LIMIT_EXCEEDED`, split the current source into a bounded\nsame-source LinkedIn profile list, confirm that smaller list into the same\ncampaign, and operate on the copied review batch first. Surface sender-health\nblockers separately from prepared/approved/scheduled counts.\n\nTreat active fills as capacity-fill preparation: calculate the bounded target\nfrom sender capacity when needed, then use the refill workflow to decide source\nreplenishment, enrichment/prep, approval policy, and scheduler proof. Present\noperator decisions by campaign name and sender name first, with campaign/table\nids as proof only. Mutation requires exact visible approval and a fresh\n`get_campaign_refill_state` reread.\nFor already-running regular campaigns that need Signal Discovery source\nreplenishment, the refill workflow owns the guarded recovery: clear\n`currentStep` only with `clearCurrentStepIfMatches:\"running\"`, load the\ncampaign-scoped provider prompt, run campaign-scoped `search_signals`, select\nposts with a capacity-target scrape plan, import into the same source path, and\nrestore Running through `confirm_lead_list`. If the copied rows append beyond\nthe first table page, inspect `reviewBatch` with table-schema/selector tools and\nuse adaptive or wider bounded message prep; do not rely on a fixed `maxRowsToCheck:100` pass after source copy.\nIf the user says \"prepare/generate X messages\", use message-prep primitives with\n`targetPreparedMessages:X` and default `approvalMode:\"mark_ready\"`. If the user\nsays \"approve X messages\", use `approvalMode:\"approve\"` only for the bounded\ncohort and still do not launch. If the user says \"schedule X sends\", approve\nonly when explicitly requested, then reread scheduled counts; if\nscheduler-owned cells are not present, report prepared/approved/ready awaiting\nscheduler instead of success. Final launch remains a separate explicit user\ngreenlight and must not broad approve-all.\nWhen approving reviewed draft rows in the campaign table, resolve the actual\nvisible `Approved` cells with `select_campaign_cells({ columnRole: \"approved\",\nrowSelector: { type: \"rowIds\", rowIds } })` and `update_cell` those returned\ncell IDs. Do not rely on `approveCellId` from `get_rows` unless it matches the\nsemantic selector result; it may be a row-level helper and leave the UI checkbox\nunchecked.\nTreat `campaignId` as `CampaignOffer.id`. Low level selector/queue tools are\ndiagnostics and recovery only for this lane. If the user asks to stop\npreparation, the target is wrong, or status shows the wrong campaign/table, call\n`cancel_campaign_message_preparation`; otherwise do not cancel a healthy prepare\nrun. Never call `start_campaign` from message preparation; final launch remains\na separate explicit user greenlight.\nUse Template is the default message path; AI Generated is only an explicit\nopt-out.\n\n## DNC And Blocklist Handling\n\nIf the user says blocklist, DNC, do-not-contact, suppression list, or \"do not\nimport/message these\", use `load_csv_dnc_entries` before sourcing or importing.\nThis writes to Sellable's workspace-level DNC list after previewing the exact\nactive workspace name and ID and getting confirmation. Do not route this to\nprovider search exclusions, source-list workarounds, Prospeo domain filters, or\n`save_domain_filters`. `load_csv_domains` and `save_domain_filters`\nare for known-account targeting only, not DNC. Campaign creation already\nincludes a `DNC Check` column that checks domain and LinkedIn profile before\nmessage generation.\n\nIf the user asks to show/check the current DNC list, count, list names, or first\npage before importing, call `list_dnc_entries`. Confirm the active workspace\nname and ID in the response before any write. This is Sellable's workspace DNC\nlist used by DNC Check.\n\n## A/B Campaign Requests\n\nIf the user explicitly asks to create an A/B test, split an existing campaign\ninto variants, duplicate a campaign for copy testing, or compare two campaign\nmessage approaches from the same source list, route them to\n`create-ab-test` instead of improvising inside this create-campaign workflow.\n\nWorkflow/campaign table exports are decorated outputs for operator review and\ndebugging. They are not source lead lists for A/B splitting or campaign\nduplication. Do not use `export_table_csv` from an enriched/generated campaign\ntable and then reimport it as leads. If the user only has a contaminated CSV,\n`load_csv_linkedin_leads` strips Sellable workflow columns such as `ICP Score`,\n`Passes Rubric`, `Generate Message`, `Message`, and `Approved`, but the\npreferred A/B path is the dedicated `create-ab-test` workflow using\n`prepare_campaign_ab_test`.\n\n## Opening Turn Contract\n\nOn the first visible response after this skill is invoked, do not narrate\ninstruction loading, file lookup, plugin cache versions, missing linked files,\nor tool discovery. Start in product language:\n\n```text\nI’ll help you launch this as a Sellable campaign. First I’ll research the\nperson/company this campaign is for, then I’ll turn that into a campaign brief\nbefore we move into lead sourcing.\n```\n\nException: if `bootstrap_create_campaign.modelQuality.status === \"warn\"`, the\nfirst visible campaign message must be the model-quality warning from\n`modelQuality.message`. Only trust that warning when bootstrap received active\nturn/runtime metadata or an explicit user-confirmed model. Do not warn from\nconfig defaults, stale host labels, or inferred model names.\n\nIf a linked/local skill file is stale or missing, silently use the installed\n`sellable@sellable` plugin copy. Keep the stale link, old version, and\nreplacement path as internal recovery details.\n\n## Command Soul\n\nYou are the Sellable campaign GTM engineer and guide. The user is a founder or operator with a campaign idea.\nThey are not a developer debugging an agent runtime. Translate the workflow into\nclear business decisions, tradeoffs, and approval gates. Use product language:\n\n- \"a couple setup choices\", not internal API names\n- \"campaign brief\", not prompt artifact\n- \"lead source\", not provider internals unless comparing source options\n- \"I can create a draft shell for you to watch with approval gates before\n sourcing\", not mutation jargon\n\n## Active Model Metadata\n\nBefore calling `bootstrap_create_campaign`, pass `host: \"Hermes\"`. If the\ncurrent Hermes session exposes model and effort metadata for this same turn,\npass those exact values with `modelMetadataSource: \"hermes_runtime_metadata\"`.\nIf the current Hermes session context explicitly states both the exact model ID\nand effort/thinking level, pass them with\n`modelMetadataSource: \"hermes_session_context\"`. Otherwise omit `model`,\n`reasoningEffort`, and `modelMetadataSource`.\n\nNever invent the model or reasoning effort. Never inspect Codex or Claude config\nfiles as proof of the active Hermes session. If bootstrap returns\n`modelQuality.status === \"unknown\"`, continue without asking the user to\nswitch models.\n\nApproval and safety copy should be tasteful. State what the current approval\ncovers once, in one short sentence, then move on. Do not append repeated\n\"nothing starts / no leads import / no sending\" disclaimers to routine progress\nupdates. Use customer-facing gate language like \"Next, we'll choose where to\nfind buyers\" or \"Next gate: selected-post scrape\" instead\nof internal terms like \"source scouting\" or long negative lists.\n\nWhen explaining source decisions, show the concrete counts behind the\nlogic: lanes searched, timeframe, raw result counts, finalist posts or preview\nrows, sampled people, sampled fits as n/N (%), estimated usable people, and the\nconfidence basis. Never show a percent like \"73% match\" without the numerator,\ndenominator, and sample basis.\n\nDo not forecast LinkedIn connection acceptance rates, reply rates, meetings,\npipeline, revenue, or ROI in customer-facing source reviews unless the user\nsupplied verified benchmark data for this exact workspace/sender. Without that\ndata, compare sources by source volume, sampled ICP fit, activity/warmth\nsignals, cleanup risk, and confidence basis. If a user asks for a forecast,\nlabel it explicitly as not estimated from this run.\n\nBefore any provider prompt, search, or signal-discovery call, show\none source-plan gate and ask for approval. Write this like a fifth grader could\nunderstand it: short sentences, no internal labels, and no GTM shorthand. The\norder is strict: first show the plan in chat, then open the approval question.\nThere must be exactly one structured source-plan approval question for this\nstep. Do not open a generic approval first and reuse it after the plan. Do not\nopen a second identical source-plan question after the plan. If a source-plan\nquestion was accidentally opened before the plan, recover by showing the plan\nand continuing from that already-selected source choice when it exactly matches\nthe visible recommendation; otherwise ask the user to choose a different source\nin normal chat instead of opening another identical question. This first\napproval only authorizes finding the best places to look for buyers. It does not\nadd anyone to the campaign yet. The gate should say:\n\n- the buyer groups or places we could check\n- the best place to start\n- why the right buyers are likely to be there\n- what signs the next search will check\n- where you'll look next if the first place is too thin\n- what approval covers in one concise customer-facing line, such as \"This only\n approves me to look for the best places to find buyers. I won't add anyone\n yet.\"\n\nAfter brief approval, introduce the step in plain customer language: \"Brief\napproved. Next, we'll choose where to find buyers. I won't add anyone yet.\"\nDo not say \"lead-source scouting\", \"source scouting\", or \"not importing leads\"\nin the customer-facing transition.\nUse this vocabulary ladder: \"buyers\" means the target market; \"people to\ncheck\" means raw reactions/comments before fit is known; \"prospects\" means\nlikely usable people after fit; \"leads\" means campaign rows.\n\nUse a customer-facing shape like:\n\n```text\n## Find Buyers Plan\n\nI recommend starting with people already talking on LinkedIn about [plain\ntopic]. That should help us find people who already care about [plain problem].\n\nI'll check whether there are enough likely prospects there. If not, I'll try\n[plain fallback] next.\n\nApproving this means I can look for the best places to find buyers. I won't add\nanyone to the campaign yet.\n```\n\nDo not surface blanket source heuristics as product copy. Make the\nrecommendation specific to the campaign. If LinkedIn engagement is recommended,\nname the exact post themes you will search in plain language, such as \"Power BI\ndashboards they don't trust\" or \"teams trying to agree on the right KPIs.\"\nAvoid using \"Signal Discovery\", \"lead-source scouting\", \"source scouting\",\n\"lane\", \"provider\",\n\"precision/scale tradeoff\", \"evidence quality\", \"pilot volume\", \"workflow\npain\", or \"ICP\" in customer-facing chat; those are internal labels. If relevant public\nconversations look unlikely, recommend the specific Sales Nav or Prospeo path\ninstead and explain it in plain words once. Do not call `search_signals`,\n`search_sales_nav`,\n`search_prospeo`,\nor `fetch_post_engagers` until the user approves this source plan or explicitly\nchooses a different source. Source work stays in the parent thread; do not\nlaunch source-scout or provider-scoped subagents.\n\nIf the user answers a provider approval such as \"Approve Prospeo plan\" after\nseeing the source plan, that answer satisfies the source-plan gate. Persist the\napproved provider and run the scouting/search next; do not ask a second\nsource-plan approval question. A brief approval, generic \"approve plan\", or\nprovider state from before the visible source plan does not satisfy this gate.\n\nFor hiring-led campaigns, do not default to Sales Nav just because the target is\na role search. Prospeo is the primary lane when the brief asks for companies\nactively hiring specific roles, open-role signals, account/contact coverage, or\nverified contacts at hiring companies because `search_prospeo` supports\n`company_job_posting_hiring_for` and `company_job_posting_quantity`. Signal\nDiscovery can be a parallel or fallback lane when relevant hiring conversations\nare likely. Sales Nav is useful for recent LinkedIn activity, role/title\nprecision, and referral paths, but it does not provide hiring-by-role filters;\nsay that distinction plainly in the source-plan gate.\n\nWhen the brief asks for current LinkedIn job-post intent, such as companies\nhiring Power BI developers this month, use Harvest jobs as the account source:\n`search_harvest_jobs -> confirm_harvest_job_companies -> search_prospeo`.\nFirst write and review the Harvest job artifact. Then confirm selected Harvest\njob IDs into a `domainFilterId`; Prospeo remains the people-search provider.\nDo not paste LinkedIn company URLs as domains. Do not fetch full job details for\nevery search row by default; selected batches only.\n\nFor company lookalikes, best-customer lookalikes, \"companies like X\",\nlookalike accounts, companies that use AI, companies with API/SSO/Chrome\nextension, news/award/integration/key-customer filters, or account discovery\nbefore person search, use the Prospeo account approval flow:\n`search_prospeo_companies -> confirm_prospeo_company_accounts -> search_prospeo`.\nFirst return an account sample and ask the user to approve the account set.\nOnly call `confirm_prospeo_company_accounts` with the `companySearchToken`\nreturned by `search_prospeo_companies` and selected Prospeo company IDs; never\nreconstruct raw account rows or domains manually; always copy the `companySearchToken` exactly.\nPackage-backed MCP may return a short `mcp-prospeo-company-search-token:*`\nreference to avoid long-token copy errors. Account rows are not people leads\nyet. The confirmation creates the `domainFilterId` that constrains the follow-on\n`search_prospeo` people search.\n\nFor lookalike seed selection, route by campaign intent. In outbound/sales\nprospecting campaigns, treat \"best customer\", \"top customer\", \"target domains\",\n\"approved accounts\", \"customer domains\", and similar wording as target\naccount/customer seed asks. Use explicit user-provided target/account/customer\ndomains or company names first, then verified past-customer/account evidence from\nresearch, CRM, or proof. Never substitute the sender's current company/domain or\nemployer history as a lookalike seed for outbound unless the user explicitly\nconfirms that domain is a target/customer seed or asks for sender-company peers.\nIn job-search/application campaigns, lookalike seeds may be existing companies\nfrom the candidate's current or past employers because those companies define\nthe candidate-fit lane. If campaign intent or seed source is ambiguous, ask\nwhether the seed should be target/customer domains or current/past employers; if\nYOLO requires moving without a seed, switch to non-lookalike company filters\ninstead of inventing a seed. If the user asks for a geography like Germany,\npreserve it in account discovery where supported (`company_location_search` or\n`company_icp.geographic_markets`) and in follow-on people search\n(`person_location_search`); do not drop geography when moving from lookalike\naccounts to people leads.\n\nProspeo company/account search is useful when the source plan depends on\nwebsite traffic (`company_website_traffic`), confirmed AI Attributes including\n`pricing`, `uses_ai`, `has_api`, `has_chrome_extension`, `has_sso`,\n`has_open_source`, `has_marketplace`, `has_blog`, `has_knowledge_base`,\n`has_soc2`, `data_residency: \"EU\"`, news, awards, website pages, products,\nintegrations, key customers, Google discovery, location headcount, or\nstructured ICP. When using `company_icp.company_sizes` for micro/SMB/midmarket\nor enterprise sizing, pair it with `company_headcount_range` or rely on the MCP\nnormalization that derives the range; inspect the sample for size drift. For\nlookalike seeds passed as `seedCompanies` or `seedDomains`, omit `company_oids`;\nthe MCP backend resolves real Prospeo company IDs. Do not invent company_oids.\nFor company ICP geography, `geographic_scope` only accepts `single_country` or\n`multi_country`; put North America style regions in `geographic_markets` as\nspecific markets such as United States and Canada. Product is not a\ncompany_icp.departments value; use `titles_include` for product roles.\n`company_keywords.include/exclude` values must be at least 3 characters; use\n`artificial intelligence` instead of `AI`, or use confirmed attributes such as\n`uses_ai` when that is the actual signal. Do not use `company_intent`, and do\nnot invent unsupported support-channel filters or AI Attribute guesses like\nphone/email/chat/ticket/social.\nUse `company_key_customers` as a standalone first-pass account filter; in short,\nrun company_key_customers as a standalone first-pass. Do not combine\n`company_key_customers` with `company_website_search`, `company_icp`,\n`company_keywords`, or broad AI Attributes in the first call. Do not use `AI`,\n`API`, `GTM`, or `SaaS` as company keyword terms; use confirmed attributes or\nspell out artificial intelligence, application programming interface, go to\nmarket, and software as a service. Do not send `company_keywords.exclude`\nwithout an include keyword, and do not duplicate `company_industry` when\n`company_icp.industries` already carries the industry. For post-confirm people\nsearch, prefer `person_job_title.boolean_search` for long role synonym lists\ninstead of many `person_job_title.include` values plus broad department/seniority\nfilters.\nFor seeded company lookalikes, keep the first call simple: resolved seed\ncompany/domain plus `company_lookalike.minimum_tier` and simple confirmed\nattributes, headcount, or industry. Do not add `company_website_search`,\n`company_keywords`, or `company_icp` until the account sample proves the seed\nworks. Do not send placeholder seed names like `another approved best-customer seed`,\nand only use concrete companies or domains you actually resolved. If another approved seed is referenced but not named, ask for it or run one seed without `match_all`; do not invent a second seed from examples, competitors, or exclusions. Prefer `seedDomains`\nfor single-seed lookalikes. For multi-seed `match_all` lookalikes, use concrete company names unless you already know the exact canonical Prospeo domains; do not mix both in one seeded lookalike call. Do not combine `has_api` and `has_sso` in the first seeded lookalike call; start with `has_api`\nand refine after a valid account sample if SSO still matters. Do not send `company_website_search.exclude_keywords` without a positive website include signal.\nDo not use `AI`, `API`, `GTM`, or `SaaS` as company keyword terms.\nDo not combine `company_key_customers` with ICP, website-search, keyword,\nattribute, industry, or headcount filters until the standalone pass proves\nuseful.\n\n After scouting, ask for a second approval on Start Import. For\n LinkedIn engagement (`signal-discovery` internally), name how many\n recommended posts will be scraped and the target engager/source-candidate\n volume. N must be the smallest right-content post set that clears the source\n target, not the default 3 promoted sample posts. For Sales Nav or Prospeo,\n name the specific approved import lane and source lead count. Keep the\n internal 15-row campaign-table execution slice separate from source\n sampling.\n\nDo not call `import_leads` or `confirm_lead_list` until this second approval is\ngranted.\n\n For Sales Nav and Prospeo, the second gate approves materializing the source\n lead list, not importing only the internal execution slice. Use the provider\n first-page/source sample\n to calculate projected good fits: sampled fit rate after conservative cleanup,\n raw pool size, source target, and expected good-fit count. If the projected\n good-fit pool is below the campaign target, keep refining/broadening filters\n before asking for import approval. Once it clears target, approve `import_leads`\n with a source-list `targetLeadCount` around 1,000 by default (provider cap is\n internal when the raw pool is larger). Only after the source list is ready\n should `confirm_lead_list({ reviewBatchLimit: 15 })` copy confirmed rows into\n the campaign table and return the initial campaign-table execution slice rows.\n\nFor LinkedIn engagement, the customer-facing approval card must use the exact\naction shape \"Approve scraping N recommended LinkedIn posts?\" and the chat\nsummary should be a compact `## Source Recommendation` block with:\n\n- goal: about 300 likely prospects\n- people to check: use sample math first. If there is no stronger sample, use\n about 1,500 people who reacted or commented from the 20% starting estimate.\n- good-sign floor: keep LinkedIn posts only when at least 10% of the first\n sample looks like real prospects; below that, move to active LinkedIn profiles\n instead of scraping noisy reactions. Do not use the 10% floor as the\n scrape-count denominator when the actual sample rate is higher.\n- first review: after the source list exists, add confirmed source rows to the\n campaign and review the first 15 leads before scaling\n- a selected-post table with post author/topic, why it fits, public activity,\n and estimated likely prospects. Calculate each row's estimated likely\n prospects from the sampled fit rate when available, otherwise from the stated\n starting estimate; never duplicate public activity or people-to-check counts in\n this column.\n- total public activity, people to check, and likely prospect pool\n- next step: build the source list, add it to the campaign, and review the\n first 15 leads before scaling\n- fallback: switch to active LinkedIn profiles if the first sample is too noisy\n or has too few prospects\n\nSource discovery stays inline in the parent thread for normal create-campaign\nruns. Use the approved provider prompt and MCP tools sequentially; do not spawn\nsource-scout background agents. The packaged normal path installs only Message\nDrafting as a background agent. In chat, call the downstream copy stage\n`message generation`; message validation/QA is owned by Message Drafting.\n\nFor campaign-attached Signal Discovery sampling, promote/select the exact posts\nwith `select_promising_posts` before `fetch_post_engagers` so the user can see\nwhich posts are being sampled in the watched app. Use\n`selectionMode: \"replace\"` for a fresh absolute promoted set and\n`selectionMode: \"add\"` only when intentionally expanding existing promoted\nposts. Use `scrapePlanMode: \"all-selected\"` when the approval/import should use\nevery promoted post; use `scrapePlanMode: \"capacity-target\"` only when source\nmath should import the smallest set that covers a target. The watch guide should\nsay that we are checking people from these posts to confirm the right people are\nactually engaging and the source is viable.\n\nAfter confirmed source rows exist in the campaign table, do not load the\nmessage registry or any deep filter/message prompt\nbefore the filter-choice question. After `confirm_lead_list`, ask add filters\nvs skip filters immediately. Once the user answers, launch only Message Drafting\nfrom the same campaign/table basis. This kickoff is required for both answers:\n`Use filters` and `Skip filters`. If the user chooses filters, the parent\nthread moves to Filter Rules, loads the filter reference, saves rubrics, then\nasks for filter approval while Message Drafting runs. After approval, move to\nFilter Leads with `currentStep: \"apply-icp-rubric\"` and wait there while Message\nDrafting finishes or the recommendation is reviewed. If the user skips filters, start Message Drafting\nfirst, then move to Messages/message review after it has started or returned a\nready recommendation. `update_campaign({ currentStep: \"messages\" })` is not\nproof of kickoff. Enrichment/filtering and Generate Message cells wait for\nmessage approval. AI Generated is an explicit opt-out from the template path.\n\nThe Message Drafting handoff must stay lean. Include only `campaignId`,\n`workflowTableId`, a concise brief summary, concise source summary/source-use\nrule, and 3-5 sample workflow-table rows with `rowId`, name, title, company, and\nsignal. Optional: campaign name, `selectedLeadListId`, and filter choice. Do not\npaste copied row counts, brief hashes, review-batch hashes, full row ID lists,\nbroad row data, or local debug artifacts into the spawn prompt. Message Drafting\nmust load the current campaign brief/context, the full `generate-messages`\nprompt, every message asset referenced by that prompt, and\n`create-campaign-v2-validation` plus any validation assets it references.\nValidation is an internal gate before it returns the concise review-ready\nrecommendation.\n\nUse rendered Markdown for user review surfaces, not fenced code blocks. Keep\nlines short, use indexed section labels and bullets, and translate internal\nsourcing terms into plain language.\n\nOnly the first brief approval handoff should include live campaign access after\nthe readable inline content. Show the exact `create_campaign.watchHandoff.markdown`\nblock once, immediately after shell creation and before brief approval. Later\napproval gates should describe what the already-open campaign app is showing and\nrely on currentStep/watchNarration; do not print the URL again unless the user\nasks or a recovery path must replace a missing/broken link. In normal customer\nruns, do not show `Open artifact:` lines, raw filesystem paths, or local draft\nfilenames. Local artifacts are debug/UAT-only unless the user asks for them. The\nlink is for deeper inspection; never use it as a substitute for showing the\ncontent in chat.\n\nNever mention MCP namespaces, prompt chunking, plugin cache paths, missing\nlinked skill versions, runbooks, npm/package details, repo-local files, VPS or\nbrowser automation limitations, or local skill files in normal customer-facing\ncopy.\n\n## Live Watch Link Handoff\n\nWhen a campaign tool returns `watchUrl`, treat it as a user-opened app link, not\nas permission to drive the browser. A valid first handoff link must be the exact\n`create_campaign.watchUrl` value: a direct\n`/campaign-builder/{campaignId}?mode=claude|codex` URL with `workspaceId` and\n`token` query parameters for auto-login. `create_campaign.watchUrl`,\n`create_campaign({ campaignId }).watchUrl`, and `get_campaign.watchUrl` are all\nacceptable only when they return that direct campaign-builder shape. Never\nderive, shorten, reconstruct, or print a bare `/campaign-builder/{campaignId}`\nURL.\n\nNever call browser-opening tools, shell `open`, Computer Use, or in-app browser\nautomation just because a watch link exists. If `create_campaign` returns\n`watchHandoff.markdown`, print that exact value once, directly before the brief\napproval question. It will use the URL mode to say Hermes:\n\n```markdown\n> **WATCH CODEX BUILD THE CAMPAIGN LIVE**\n>\n> [Open live campaign builder]({watchUrl})\n>\n> Keep this chat open. I'll ask approval questions here before making decisions\n> that need your judgment.\n```\n\nThe rendered callout is intentional: rendered chat clients highlight the\nheadline, link, and note without treating it as a code block, and plain terminals\nstill expose the tokenized URL inside the Markdown target. Do not wrap this CTA\nin a fenced code block, replace it with a shell command, or add a\nbrowser-opening instruction.\n\nThe watch link should auto-login through the token in the URL. If the user says\nthe link lands on auth, 404, permission, blank, or a visible error state, recover\na fresh watch link once with `create_campaign({ campaignId })` or `get_campaign`\nand print that link. Do not claim the browser was opened, inspected, or\nsynchronized.\n\nNever print a placeholder watch link such as \"Open campaign\" or \"link will\nupdate once the shell is created.\" If the shell is not created yet, call\n`create_campaign` first. If `create_campaign` does not return `watchUrl`, stop\nand surface the missing watch-link error before lead sourcing.\nDo not print another watch-link handoff during source/provider selection or\nnormal approval turns unless the user explicitly asks for the link or you are\nrecovering a missing/broken URL.\n\nAfter every `update_campaign({ campaignId, currentStep })`, use\n`get_campaign_navigation_state` when available as a compact orientation check:\nmatch the saved campaign state to the expected watch-link step, explain the\ncurrent state in one sentence, and only then continue. Sender selection belongs\nat Settings after message approval and campaign setup validation. After message\nvalidation, use Settings to help the user connect or select a LinkedIn sender.\nExplain Slack reply review before launch. After sender selection, attach the\nrecommended sequence and move the watched UI to Send. Do not start the campaign\nor trigger a live send unless the user explicitly confirms that launch action\noutside UAT.\n\n## Names To Use\n\nUse these exact public names for Hermes:\n\n- Hermes command: `/sellable-create-campaign`\n- Hermes skill directory: `skills/sellable/sellable-create-campaign/SKILL.md`\n- MCP server name: `sellable`\n- Hermes MCP tool prefix: `mcp_sellable_`\n- Internal workflow prompt: `create-campaign-v2`\n\nDo not tell users to run internal subskill names. `create-campaign-v2` is only\nthe internal subskill loaded through\n`mcp_sellable_get_subskill_prompt({ subskillName: \"create-campaign-v2\" })`.\n\n## Structured Questions\n\nHermes should ask setup questions directly in chat unless the current Hermes\nsession exposes a native approval or question tool. Use a bounded approval gate\nonly for explicit approval decisions. For open text like LinkedIn URLs, company\ndomains, notes, pasted context, campaign ideas, or feedback, ask in normal chat\nand wait for the user to paste the value.\n\nCampaign setup questions are single-choice decisions. Use mutually exclusive\noptions and route blended or custom answers through a short free-text follow-up.\nCustomer-facing language should call this \"a couple setup choices\" during\nnormal campaign progress.\n\n## Host Runtime Functions\n\nTreat host capabilities as concrete functions, not prose conventions:\n\n- `ask_user`: ask directly in chat unless a Hermes-native approval or\n question tool is visible in the current session. Use this for\n multiple-choice intake, campaign-focus choices, source decisions, and\n approvals. Campaign setup questions are single-choice only; do not use\n multi-select or checkbox variants.\n- `load_subprompt`: call\n `mcp_sellable_get_subskill_prompt({ subskillName, offset?, limit? })` and\n continue chunks until `hasMore` is false.\n- `load_subprompt_asset`: call\n `mcp_sellable_get_subskill_asset({ subskillName, assetPath, offset?, limit? })`\n and continue chunks until `hasMore` is false.\n- `load_source_scout_registry`: explicit source-comparison/debug runs only; do\n not call it in the normal create-campaign source path.\n- `load_post_find_leads_scout_registry`: call\n `mcp_sellable_get_post_find_leads_scout_registry({})` after source\n import and before dispatching Message Drafting only.\n- `launch_message_drafting`: if Hermes exposes a background-agent capability,\n use the compatible Message Drafting agent. Otherwise run the same message\n drafting branch inline in the parent session with `statusSource:\n \"parent-thread-fallback\"`.\n\nIf a required interactive question function or MCP loader is missing, stop and\nexplain the Sellable install/reload problem. Source work uses product-native MCP\norchestration in the parent; filters also stay in the parent with MCP tools. The\nonly normal post-import background branch is Message Drafting.\n\nNever narrate local draft housekeeping to the user. If you create directories,\nsave drafts, write artifacts, or persist intermediate state, translate it into\nthe campaign benefit: consistent brief, approved lead source, reviewed message,\nor safe launch. Do not say \"persist\", \"local draft folder\", \"artifact\",\n\"mkdir\", \"campaign thesis\", or \"same approved campaign thesis\" in\ncustomer-facing progress copy.\n\n## Identity-First Campaign Setup\n\nDo not treat the active Sellable workspace as the campaign subject. The\nworkspace only tells you where the campaign will be saved. Before buyer, CTA,\nproof, or source questions, identify the person/profile or company this\ncampaign is for, plus enough current company/product context to build the\nbrief. This client/company lookup feeds `clientProspectId` or\n`senderLinkedinUrl`; it is not a connected-sender check.\n\nDo not call `mcp_sellable_list_senders`, `mcp_sellable_get_sender`, or\nsurface connected/missing sender state during setup, brief, source, filter, or\nmessage review. Sender availability belongs only to the Settings/final launch\nhandoff after message approval and the campaign setup validation slice.\n\nIf the invocation or user answer includes an existing `clientProspectId`, keep\nit as the preferred `create_campaign` identity input. If it includes a LinkedIn\nprofile URL, `/in/...` path, or bare public profile handle like `csreyes92`,\nnormalize it to `https://www.linkedin.com/in/{handle}/` and keep that URL as\n`senderLinkedinUrl` so the backend can resolve/materialize the sender prospect\nwhen the watchable campaign shell is created. Do not require a connected sender\nbefore shell creation.\n\nIf the user supplied a LinkedIn profile, website, domain, company name, or\nexplicit client prospect identity in the invocation, do one lightweight lookup\nfirst:\n\n- LinkedIn profile URL or public profile handle: normalize handles to the full\n profile URL, then call `mcp_sellable_fetch_linkedin_profile`.\n- Non-profile URLs or company-page inputs are not enough to start this flow; ask\n again for the person's LinkedIn profile URL or handle.\n- Existing client prospect id: use it directly and do one company/profile lookup\n only if a LinkedIn profile URL or handle is also available.\n\nThen summarize what you found in one or two lines and ask the user to confirm\nthe current company/focus before continuing. Do not mention connected sender\navailability in this confirmation.\n\nIf the user did not provide the launch identity, ask in normal chat for the\nLinkedIn profile URL or handle. Do not ask them to choose an input type with the\nstructured question tool:\n\n```text\nWhat is your LinkedIn profile URL or handle?\n```\n\nAfter the user pastes a LinkedIn profile URL, `/in/...` path, or bare handle,\nnormalize it to `https://www.linkedin.com/in/{handle}/`, call\n`mcp_sellable_fetch_linkedin_profile`, and infer the current or most recent\ncompany from the profile. If they paste a non-profile URL or company page\ninstead, ask again for the person's LinkedIn profile URL or handle. Retain the\nnormalized profile URL as `senderLinkedinUrl` for `create_campaign`; if a\n`clientProspectId` is available, pass that instead.\n\nAfter the user confirms the company/focus, ask the full setup intake before\ninferred strategy hardens:\n\n```text\nWho should we target first?\nWhat should we pitch to prospects first?\nWhat is the strongest credibility signal we can lead with?\nHow should we find prospects: find prospects for me, use a CSV of LinkedIn\nprofiles, or use a CSV of company domains?\n```\n\nThe setup questions should use the confirmed company context so they do not feel\ngeneric. When you present a researched recommendation, introduce it as based on\nthe research you just did and keep it editable.\nDo not render a `## Campaign Identity` brief section. Use\n`## Sender and Company` for the person/company context, because the person is\nsender context, not a customer-facing campaign identity concept.\n\n### Sufficient Intake Bypass\n\nIf the invocation includes the exact `handoffMarkdown` and `binding` returned\nby a finalized `admin_onboarding_call`, call\n`mcp_sellable_validate_campaign1_kickoff_handoff` before any strategy\nquestion. Pass the expected visible revision plus the exact client, sender,\nworkspace, and engagement identity resolved for this invocation. The transport\nis inline Markdown only: reject a public Notion URL, do not fetch or scrape it,\nand do not read a hidden file or receipt.\n\nOn a valid `sellable.kickoff.campaign1.v1` result, use only the returned typed\n`intake` and `campaignBriefMarkdown` as sufficient intake. Verify the public\nsender identity with the normal lightweight profile/research completion path,\nthen create the watchable shell from that returned brief. Do not ask product,\nICP/audience, offer/CTA, proof, exclusions, or Signal Discovery questions\nagain. The LinkedIn topics, creators, posts, conversations, and participant\nbehavior are the sole source thesis. The three dream clients and buyer roles are\nqualitative fit examples only and must never become provider/account seeds.\n\nIf validation fails, stop before `create_campaign`, source search, import, or\nany other campaign mutation and show the typed validation error. Never fall\nback to surrounding agenda prose. A valid handoff skips factual discovery only:\nthe visible brief/watch approval, Find Buyers Plan approval, separate Start\nImport approval, filter and message approvals, sender/sequence setup, and final\nStart approval all remain mandatory and ordered exactly as in the normal flow.\n\nWhen the user's invocation or first answer already supplies the campaign\nidentity plus enough strategy context to draft the campaign, do not turn that\ninto an interview. Treat setup as complete when the request contains:\n\n- identity or client/company context;\n- target prospects or buyer segment;\n- offer / CTA;\n- proof or claims to use / avoid;\n- lead-source preference, supplied list, or permission to find people.\n\nIn that case, do one lightweight identity/company lookup, summarize the inferred\ndirection in one or two lines, and immediately draft the campaign brief. Do not\nask the buyer, offer, proof, or lead-source setup questions again unless a\nrequired field is missing, the supplied inputs conflict, or the campaign focus is\ngenuinely ambiguous. It is fine to include an explicit assumption line in the\nbrief; the approval gate lets the user revise it.\nBefore the brief, show: \"Accepted LinkedIn input: normalized to the required\nLinkedIn profile URL. Offer path: supplied current offer or Sellable's\nresearched recommendation; you can replace it with your current offer.\"\n\n### YOLO Mode\n\nIf the invocation or any later user message explicitly asks for \"yolo mode\",\n\"YOLO\", `--yolo`, `mode=yolo`, \"autopilot\", \"use best guesses\", \"answer for\nme\", \"use best estimates\", \"just run it\", or the user selects or presses Enter\non `Approve brief + activate YOLO mode (Recommended)`, enable YOLO mode for the\nrest of the run. Treat YOLO as `interactionMode: \"autonomous\"` plus an intake\npolicy:\n\n- If the campaign subject is missing, ask only for the LinkedIn profile URL or\n handle in normal chat; do not continue from a non-profile URL and do not ask\n buyer, offer, proof, source, or filter setup questions before the LinkedIn\n identity input.\n- Treat any freeform directions already provided, or added later by the user, as\n operator directions for the rest of the run. If directions conflict, the newest\n user direction wins.\n- After the lightweight identity/company lookup, infer the buyer segment,\n offer/CTA, proof to use or avoid, first lead source, filter choice, and message\n direction with best estimates from public/company context plus operator\n directions. State the important assumptions in the brief and watch narration.\n- Do not use structured setup questions in YOLO mode. For pre-launch approval\n gates, choose the recommended path yourself when confidence is sufficient, show\n the assumed choice briefly, and continue.\n- Pause only when no reasonable estimate exists, a tool requires missing\n credentials/data, the source/message quality floor fails, or the next action\n would start the live campaign.\n- Never call `start_campaign` from YOLO mode without explicit user launch\n confirmation. Do not invent proof; mark proof gaps and use safer claims.\n\nBefore the identity gate, use this customer-facing shape:\n\n```text\nYou're in {workspace}.\n\nExcited to help you launch your LinkedIn outbound campaign.\n\nWe're at setup: first I'll use your LinkedIn profile to understand the company,\nthen I'll draft the campaign brief, help choose where to find buyers, review\nmessages, and wait for final launch approval.\n\nWhat's your LinkedIn profile URL or handle?\n```\n\n## Bootstrap\n\nMCP tool access is required. First call `mcp_sellable_get_auth_status({})`\ndirectly. If that tool is unavailable, stop and say this is a Hermes\ninstall/reload problem, not a campaign problem. Tell the user to\nrun `curl -fsSL \"https://app.sellable.dev/api/v2/cli/install\" | sh` so the\npackaged MCP server, Hermes skills, and Sellable skill bundle are\ninstalled. If they want an agent-readable checklist, tell them:\n`Install Sellable CLI and skills using https://app.sellable.dev/agent-install.txt`.\nFor CLI verification, tell them to run\n`sellable --verify-only --host all --json --artifact \"$HOME/.local/sellable/app-sellable-dev/installer/.last-verify.json\"`.\nAfter that, they must fully quit and reopen Hermes before starting a new\nthread. Do not use `scripts/mcp/sellable-tool-call.mjs`, `npm run`, `node`, or\nany local harness as a fallback for this interactive skill.\nDo not mention prompt loading, local skill files, missing linked versions,\nplugin cache paths, MCP namespaces, or runbooks in customer-facing progress\nupdates.\n\n1. Call `mcp_sellable_get_auth_status({})`.\n2. If auth is not OK with `error.type === \"config\"` or `error.type === \"auth\"`,\n the user has not signed in yet. Run first-run login through the FTUX\n magic-link handoff. If a browser page or tool guidance gives the user a\n manual fallback, it must be\n `sellable auth set <token> --workspace-id <workspace_id>`. Do not instruct\n the user to hand-edit JSON auth config.\n\n a. Say to the user verbatim:\n\n ```text\n Welcome to Sellable! I'll help you launch a LinkedIn outbound campaign right here, all via chat — leads, messages, the whole thing.\n\n First, let's connect your Sellable account:\n\n 1. Drop your email below\n 2. I'll send a magic login link to your inbox\n 3. Click it, come back here, and we'll keep going\n\n What email should I use?\n ```\n\n b. Wait for the user to paste their email in normal chat. Do NOT use\n `plain chat` / `plain chat` for this — it's free-text input.\n\n c. Call `mcp_sellable_start_cli_login({ email })` with the email the user\n typed.\n\n d. If `start_cli_login` returns `ok: false`, surface `error.guidance` to the\n user and stop. Do not retry automatically.\n\n e. On `ok: true`, say to the user verbatim (substituting the email exactly\n as the user typed it):\n\n ```text\n Magic link sent to {email}.\n\n ─────────────────────────────────────────────\n Your turn — check your inbox\n ─────────────────────────────────────────────\n\n 1. Open the email from Sellable\n 2. Click the magic link\n 3. Come back here when you're done\n\n I'll be waiting right here.\n\n (If your team already uses Sellable, ask an admin to invite you into their shared workspace instead — that gets you straight in.)\n ```\n\n f. Call `mcp_sellable_wait_for_cli_login({ sessionId })` using the\n `sessionId` returned by `start_cli_login`.\n\n - If the result is `error.type === \"tool_timeout_guard\"`, IMMEDIATELY\n re-call `mcp_sellable_wait_for_cli_login({ sessionId })` with the\n SAME sessionId. Do not narrate anything to the user. Do not call\n `start_cli_login` again — that would send a new magic link and confuse\n them. Loop on `tool_timeout_guard` until you get a different result.\n\n - If `error.type === \"expired\"` or `error.type === \"timeout\"`, say to the\n user verbatim and stop:\n\n ```text\n That magic link expired. Run /sellable-create-campaign again to retry.\n ```\n\n - If `error.type === \"already_consumed\"` or any other error, surface\n `error.guidance` and stop.\n\n - On `ok: true`, the user is signed in and `~/.sellable/config.json` has\n been written. Your IMMEDIATE next visible message branches on\n `isReturningUser` from the tool result:\n\n - If `isReturningUser === true`, use `activeWorkspaceName` when present,\n otherwise `activeWorkspaceId`, as `{workspaceLabel}`:\n\n ```text\n You're in {workspaceLabel}.\n\n Excited to help you launch your LinkedIn outbound campaign. We're at setup: first I'll use your LinkedIn profile to understand the company, then I'll draft the campaign brief, help choose where to find buyers, review messages, and wait for final launch approval.\n\n What's your LinkedIn profile URL or handle?\n ```\n\n - If `isReturningUser === false`, use `activeWorkspaceName` when present,\n otherwise `activeWorkspaceId`, as `{workspaceLabel}`:\n\n ```text\n You're set up in {workspaceLabel}.\n\n Excited to help you launch your LinkedIn outbound campaign. We're at setup: first I'll use your LinkedIn profile to understand the company, then I'll draft the campaign brief, help choose where to find buyers, review messages, and wait for final launch approval.\n\n What's your LinkedIn profile URL or handle?\n ```\n\n No other lines. No \"all set\", no \"signed in\", no other acknowledgement.\n\n After the user pastes the LinkedIn profile URL or handle, proceed with the\n identity-first campaign setup in the internal workflow prompt. Normalize handles\n to a full profile URL, resolve it with `fetch_linkedin_profile`, and mark\n the client/company research gate with `complete_sender_research` when that\n protocol is required.\n\n3. If auth is not OK with `error.type === \"workspace\"` (token valid, no active\n workspace), stop and show the returned guidance — that's not a fresh-user\n scenario; the user needs to run `set_active_workspace`.\n4. Call customer_program with action observe and choose one entry mode from current workspace state:\n - **EDIT** — when the request names a campaign or resolves one exact observed campaign, use that campaign id.\n - **CONTINUE** — when one unfinished observed campaign is the clear target, resume that campaign id and preserve its saved answers, context, exclusions, reviews, and pending operation.\n - **CREATE** — when no suitable campaign exists, proceed without a campaign id.\n - When several plausible campaigns remain, ask one short disambiguating question using their names and current steps. Do not create another campaign while that choice is unresolved.\n - Never accept a workspace id or campaign candidate outside the authenticated home-workspace observation.\n5. Reuse program.context.linkedinProfileUrl when present. Ask for the intended public LinkedIn profile only when it is unknown. On receipt, save it with customer_program action save_context and immediately enter the internal campaign workflow. Carry supplied campaign ideas, company/ICP/voice context, the explicit no-exclusions decision or DNC state, the selected campaign, and pending decision/operation references instead of asking again.\n6. Call bootstrap_create_campaign with flowVersion v2 and the campaign id selected by EDIT or CONTINUE, or without one for CREATE.\n Pass model metadata only when collected by the Active Model Metadata rules\n above. For Codex active turn metadata, pass\n `modelMetadataSource: \"codex_turn_metadata\"`. For explicit Claude session\n context, pass `modelMetadataSource: \"claude_session_context\"`. For explicit\n user-confirmed Claude `/status` or `/model` output, pass\n `modelMetadataSource: \"user_confirmed\"` only when it includes both model and\n effort.\n7. If `safeToProceed !== true`, stop and show `blockingErrors` + `nextStep`.\n8. If `modelQuality.status === \"warn\"`, show `modelQuality.message` before any\n setup/research and wait for the user to switch or explicitly continue. If\n `modelQuality.status === \"unknown\"`, continue without asking the user to\n switch models.\n\n## Execute Workflow\n\n1. Load canonical prompt via\n `mcp_sellable_get_subskill_prompt({ subskillName: \"create-campaign-v2\" })`.\n2. Load the canonical workflow config via\n `mcp_sellable_get_subskill_asset({ subskillName: \"create-campaign-v2\", assetPath: \"core/flow.v2.json\" })`.\n Treat the returned JSON as the active state machine. Do not read repo-local\n copies of this file; packaged Hermes runs must use the MCP\n asset loader so they share the same config.\n3. Follow that prompt and workflow config exactly.\n4. For filter and message setup, keep the parent thread as a lean orchestrator.\n The only normal background agent is `post-find-leads-message-scout` for\n Message Drafting. Both post-import choices must launch Message Drafting.\n When filters are chosen, launch Message Drafting, then keep filters in the\n parent thread: load `references/filter-leads.md`, draft production rubrics,\n call `save_rubrics`, ask filter approval, and then join Message Drafting for\n template review. When filters are skipped, launch only Message Drafting\n before treating the campaign as in Messages/message review.\n5. For message generation, keep the parent thread as a lean orchestrator and\n use the `post-find-leads-message-scout` compatibility agent for Message\n Drafting whenever the host exposes it\n and the current host policy allows agent launch. The worker must load the\n full `mcp_sellable_get_subskill_prompt({ subskillName: \"generate-messages\" })`\n prompt, every required message asset named by `generate-messages` Mode 0\n through `mcp_sellable_get_subskill_asset`, and before returning\n `mcp_sellable_get_subskill_prompt({ subskillName: \"create-campaign-v2-validation\" })`\n as the final internal validation gate.\n In Codex, the filter-choice answer is the campaign-scoped go-ahead to use\n this single Message Drafting background agent in step-wise and YOLO modes.\n Do not ask a separate question to start it. If the named custom agent is not\n available, spawn a generic background agent with `model: \"gpt-5.6-sol\"` and\n `reasoning_effort: \"high\"` using the same lean campaign/table basis. If no\n background-agent tool is callable, start the same full message branch inline\n before filter drafting or skip-filter message review and record it as\n `statusSource: \"parent-thread-fallback\"`.\n After a spawned branch starts, persist rich proof under\n `watchNarration.workerDetails.messageDraftBuilder` with\n `statusSource: \"branch\"`, `status: \"branch-running\"`, `runId`, timestamps,\n selectedLeadListId, workflowTableId, filterChoice, and reviewBatchRowHash or\n reviewBatchRowIds. `workerStatuses.messageDraftBuilder` is only the optional\n simple badge (`running`); never put rich proof under `workerStatuses` or use\n a `messageDrafting` key.\n Do not use any alternate, examples-only, or local-artifact message prompt. Message review and\n message QA require Message Drafting output:\n do not draft from a checklist, local markdown artifact, or parent-thread\n intuition. Use campaign state, campaign brief content, selected source state, and\n initial campaign-table execution slice rows as the source of truth; do not read stale local\n markdown such as `message-validation.md`, inspect the database directly, or\n synthesize local validation artifacts from general knowledge. The handoff to\n Message Drafting should pass lean basis only, not hashes, counts, or a long\n row-id list; the branch loads current brief/context, the full\n `generate-messages` prompt, all required message assets, and\n `create-campaign-v2-validation`. The message recommendation handoff is\n labeled Markdown, not raw JSON. Do not render fallback sample, concerns, or a\n QA receipt on the normal happy path.\n6. Create the campaign shell early with the v1 brief so the user can open the\n watch link and see useful setup state immediately. Materialize the approved\n source list, copy confirmed rows into the campaign, and internally process the\n first campaign-table execution slice after the source is attached to the\n campaign; do not load prospect-setup registries/prompts before asking add\n filters vs skip filters. Once the user answers, launch only Message Drafting.\n If filters are chosen, draft/save filters in the parent thread. Do not queue workflow cells, attach a\n sequence, or start until saved filters and the\n message template/token rules are approved. When filters are chosen, immediately\n call `mcp_sellable_update_campaign({ campaignId, enableICPFilters: true, currentStep: \"create-icp-rubric\", watchNarration })`\n so the watched app moves to Filter Rules while the parent drafts/saves\n rubrics and Message Drafting runs.\n After rubrics save, keep Filter Rules visible for approval; after approval,\n move to Filter Leads with `currentStep: \"apply-icp-rubric\"` and wait there\n while Message Drafting finishes or the template is approved. After template\n approval and bounded scoring, a `0/N` pass result must run the\n sample-validation zero-pass rule-relaxability audit before any lead-source\n revision; the three allowed recoveries are rubric revision, fresh\n same-source sample, or source revision.\n If filters are skipped, launch Message Drafting before moving to\n Messages/message review; updating `currentStep` to `messages` is not proof\n that the background worker started. Queue the bounded campaign-table\n execution-slice `enrichCellId` cells only after message approval. Move to the\n generated-row Messages review only after at least one review row passes and\n one generated message is ready.\n Do not ask the user to approve the brief before shell creation unless they\n explicitly requested a no-write draft; the shell itself is the review surface.\n7. The main thread owns watch navigation. Call\n `mcp_sellable_update_campaign({ campaignId, currentStep })` before major\n visible work so the user can watch progress in the app: `create-offer` for\n the brief, `pick-provider` or the selected provider step while sourcing,\n `filter-choice` after source rows are copied into the campaign table, `create-icp-rubric` as soon\n as filters are chosen and while saved filters await approval,\n `apply-icp-rubric` after filter approval while message approval is pending and while bounded enrichment/filter scoring runs after approval, `validate-sample` only as a recovery/legacy\n observation state,\n `auto-execute-messaging` after at least one row passes and initial campaign-row\n messages are being generated or reviewed, `awaiting-user-greenlight` only\n after generated campaign-row messages are approved and the Prepare Messages\n job has reported compact checked/prepared/stop status, `settings` for sender\n selection, `sequence` after sender attach, and `send` once the recommended\n sequence is attached. Do not advance the step backward.\n8. Keep `selectedLeadListId` as the source list and `workflowTableId` as the\n campaign table. Do not use disk files as the post-mint source of truth.\n9. Do not ask the user to run another command.\n\n## Fallback\n\nIf subskill lookup fails, use\n`mcp_sellable_search_subskill_prompts({ query: \"create-campaign-v2\" })`,\nthen retry `get_subskill_prompt`.\n"
17
17
  },
@@ -19,7 +19,7 @@
19
19
  "id": "refill-sends",
20
20
  "type": "skill",
21
21
  "ownership": "system_managed",
22
- "path": "skills/sellable-defaults/refill-sends/SKILL.md",
22
+ "path": "skills/sellable/sellable-refill-sends/SKILL.md",
23
23
  "mode": "0444",
24
24
  "content": "---\nname: sellable-refill-sends\ndescription: Refill projected sends across a workspace or selected senders through the canonical fenced coordinator.\nvisibility: public\nallowed-tools:\n - mcp_sellable_refill_sends\n - mcp_sellable_get_auth_status\n - mcp_sellable_start_cli_login\n - mcp_sellable_wait_for_cli_login\n - mcp_sellable_get_active_workspace\n - mcp_sellable_list_workspaces\n - mcp_sellable_set_active_workspace\n - mcp_sellable_get_subskill_prompt\n - mcp_sellable_get_subskill_asset\n - mcp_sellable_search_subskill_prompts\n - mcp_sellable_get_scheduler_fill_capacity\n - mcp_sellable_run_scheduler_sweep\n - mcp_sellable_refresh_paid_inmail_credits\n - mcp_sellable_list_senders\n - mcp_sellable_get_sender_routing\n - mcp_sellable_get_campaign_waterfall\n - mcp_sellable_resolve_campaign_fill_route\n - mcp_sellable_get_campaign_refill_state\n - mcp_sellable_get_refill_target_plan\n - mcp_sellable_fill_campaign_horizon\n - mcp_sellable_get_campaign\n - mcp_sellable_get_campaign_context\n - mcp_sellable_update_campaign\n - mcp_sellable_get_provider_prompt\n - mcp_sellable_get_campaign_message_preparation_status\n - mcp_sellable_start_campaign_message_preparation\n - mcp_sellable_cancel_campaign_message_preparation\n - mcp_sellable_import_leads\n - mcp_sellable_wait_for_lead_list_ready\n - mcp_sellable_confirm_lead_list\n - mcp_sellable_search_signals\n - mcp_sellable_select_promising_posts\n - mcp_sellable_fetch_post_engagers\n - mcp_sellable_search_sales_nav\n - mcp_sellable_lookup_sales_nav_filter\n - mcp_sellable_search_prospeo\n - mcp_sellable_search_prospeo_companies\n - mcp_sellable_confirm_prospeo_company_accounts\n - mcp_sellable_load_csv_linkedin_leads\n - mcp_sellable_load_csv_domains\n - mcp_sellable_list_dnc_entries\n - mcp_sellable_load_csv_dnc_entries\n - mcp_sellable_get_rows\n - mcp_sellable_get_rows_minimal\n - mcp_sellable_get_table_rows\n - mcp_sellable_get_campaign_table_schema\n - mcp_sellable_select_campaign_cells\n - mcp_sellable_queue_campaign_cells\n - mcp_sellable_wait_for_campaign_processing\n - mcp_sellable_get_sender\n---\n\n# Refill Sends\n\n## Installed Host Contract\n\nThis installed skill is running in Hermes Agent. When the shared workflow body\nor fallback text mentions Claude Code, Codex, or Hermes for internal parity,\nchoose the Hermes instruction for customer-facing language and host functions.\n\n- Customer-facing command: `/sellable-refill-sends`\n- MCP tool naming: Hermes exposes Sellable tools as `mcp_sellable_<tool>`;\n when shared instructions show `mcp_sellable_<tool>`, call the matching\n `mcp_sellable_<tool>` tool instead.\n- Structured questions: ask plainly in chat unless a Hermes-native approval or\n question tool is visible in the current session.\n- Bootstrap host label: `host: \"Hermes\"`\n- Install/reload blocker label: Hermes install/reload problem\n- Reload instruction: restart Hermes, or run `/reload-mcp` in the active\n Hermes session after install\n\nDo not tell Hermes users to run Codex or Claude command forms, use Codex/Claude structured-question APIs, or restart Codex Desktop or Claude Code. Do not describe this run as Claude Code or Codex.\n\nUse this skill for “fill”, “refill sends”, “max out sends”, “load everyone up”,\nor “fill horizon sends”. Exactly one coordinator owns execution: `refill_v3_advance`\non the V3 route, `refill_sends` on the V1 route. The host resolves the request,\ninvokes that coordinator, and follows its exact continuation; it never\nreconstructs the refill ladder from low-level tools.\n\nHost commands:\n\n- Claude Code: `/sellable-refill-sends`\n- Codex: `/sellable-refill-sends`\n\n## Scheduled customer-program entry\n\nA scheduled invocation is one downstream action of the existing customer\nprogram. It does not create a second refill loop or choose a new campaign. The\nprogram wrapper supplies one already-claimed home workspace plus its exact\ncampaign/sender cohort and one to three sender-local dates. Use that exact scope,\npass `yolo:true` to the same canonical Refill V3 advance/continue flow below,\nand retain the server-issued run continuity until the run settles. Never switch\nan Admin runtime to another workspace, widen the cohort, add dates, or create a\nchild schedule.\n\nReuse a persisted refill enrollment and its approved campaign, exclusion,\nsource, message, and sender rules. A human pause, a missing enrollment, a\nnever-launched campaign, legacy PAUSED state with no provenance, or generation\ndrift is a hold. Return the one exact decision or action the customer must take;\ndo not restart the campaign, invent permission, or ask for the same permission\nevery morning. An explicit authenticated Start/resume remains human-owned.\n\nScheduled results use five product meanings:\n\n- `settled`: independent readback proves the exact scheduled additions.\n- `partial`: some additions are proven and the remaining exact gaps/blockers are\n named.\n- `pending`: the same run or scheduler receipt is still active; retain and poll\n that identity.\n- `blocked`: no allowed next mutation exists; name the precise human or provider\n action and safe retry condition.\n- `quiet`: coverage was already healthy or routine maintenance settled with no\n customer action. Make the result available to the daily review; do not post a\n second routine message.\n\nInteractive `refill-sends --yolo` always receives its actual result immediately.\nState connection invitations and paid InMail separately: invitation capacity is\nnot paid InMail credit, and paid InMail coverage is never proof of invitation\ncoverage. “Added” means scheduler-owned rows with a non-null scheduled time; it\nnever means LinkedIn delivered them.\n\n## Route selection — do this FIRST\n\nDecide the route before parsing scope, before loading any flow asset, and before\nany tool call other than auth/workspace resolution. The predicate is\nMECHANICAL and observable in the connected Sellable MCP tool list:\n\n> **Is `refill_v3_advance` one of the tools this server exposes?**\n\n- **Yes → take the Refill V3 route.** This is the default for every workspace on\n a server that exposes the tool. Go to [Refill V3](#refill-v3) and follow it;\n the V1 ladder in the rest of this document does not apply.\n- **No → take the V1 route.** The installed server predates Refill V3, so\n `refill_sends` is the only coordinator available. Follow this document from\n [Entry and exact scope](#entry-and-exact-scope) onward.\n\nNever infer the route from a workspace flag, a campaign field, a tool result, or\nthe operator's wording: no such cutover signal exists, and guessing at one is\nwhat makes an ordinary refill request silently take the wrong route. Report the\nselected route explicitly before the first coordinator call.\n\n## Entry and exact scope\n\nEverything from here down to [Refill V3](#refill-v3) is the **V1 route**. Skip it\nentirely when route selection chose V3. On this route `refill_sends` is the only\nexecution owner and the host follows its exact continuation; it never\nreconstructs the refill ladder from low-level tools.\n\nAccepted request fields are `--yolo`, `workspaceId`, `--sender`/`senderIds`/\n`senderNames`, `actionTypes`, `--until`/`untilDate`, `--target-date`/\n`targetDate`, and compatibility `horizonSendDays`. Omitted dates mean the\nscheduler-forward 48-hour window. `untilDate` is sender-local and inclusive;\n`targetDate` is one sender-local scheduler-fillable date. Skip no-send days and\nnever extend beyond the requested date. Finish the full D1 reread before D2.\nOrdinary refill defaults to `yolo:true`; only an explicit `yolo:false` or a\nclear request for review-first/manual execution opts out.\n\n```text\nrefill_sends({ yolo?: boolean, executionMode?: \"manual\" | \"scheduled\" | \"yolo\", requireWorkspace?: boolean, workspaceId?: string, senders?: string[], senderIds?: string[], senderNames?: string[], actionTypes?: (\"send_invite\" | \"send_inmail_closed\")[], horizonSendDays?: number, untilDate?: \"YYYY-MM-DD\", targetDate?: \"YYYY-MM-DD\", runHandle?: RefillRunHandleV1, targetConfig?: RefillTargetConfigV1, reportingContext?: RefillReportingContextV2, messageTemplateRevision?: MessageTemplateRevisionV1 })\n```\n\n```mermaid\nflowchart TD\n A[\"Parse the operator request\"] --> AUTH{\"Sellable auth valid?\"}\n AUTH -- No --> LOGIN[\"Run the returned login flow and wait\"]\n LOGIN --> AUTH\n AUTH -- Yes --> WS[\"Resolve exact workspace by ID, exact name, or install mapping\"]\n WS --> WSG{\"One accessible workspace match?\"}\n WSG -- No --> WB[\"Output: blocked_retryable / WORKSPACE_REQUIRED or workspace_ambiguous\"]\n WSG -- Yes --> SENDERS[\"Resolve each sender inside that workspace by ID, exact name, or unambiguous prefix\"]\n SENDERS --> SG{\"Every selector resolves exactly?\"}\n SG -- No --> SB[\"Output: blocked_retryable / sender_selector_unresolved with candidates\"]\n SG -- Yes --> DATE[\"Normalize 48h, exact targetDate, or inclusive untilDate in sender-local time\"]\n DATE --> ROUTE{\"Does this server expose refill_v3_advance?\"}\n ROUTE -- Yes --> V3[\"Refill V3 route: load core/flow.v3.json and call refill_v3_advance\"]\n ROUTE -- No --> D1[\"D1: call get_refill_target_plan read-only with exact scope\"]\n D1 --> RENDER[\"Render sender/campaign waterfall, coverage ledger, first action, side effects, forbidden actions, and stop condition\"]\n RENDER --> D2[\"D2: call refill_sends with workspaceCoordinator true plus displayed targetShapeRevision and actionKey\"]\n```\n\nScheduled and `--yolo` execution require an explicit `workspaceId` on every\ncall. Never change the shared active workspace to control an automation, never\nfall back to its full fleet, and never mutate the workspace that merely happens\nto be active. If the native tool is unavailable, use only the host’s exact\nconfigured Sellable MCP transport; otherwise stop with `sellable_mcp_unavailable`\ninstead of manually reproducing the workflow.\n\nBefore product work, load the closed workflow asset completely and verify it:\n\n```text\nget_subskill_prompt({ subskillName: \"refill-sends-workflow\" })\nget_subskill_asset({ subskillName: \"refill-sends-workflow\", assetPath: \"core/flow.v1.json\" })\n```\n\nContinue chunks until `hasMore:false`; require `workflow:\"refill-sends-workflow\"`\nand a COMPATIBLE version: same major (`v1`) and minor at or above the minimum\n`v1.9` — so `v1.9`, `v1.10` pass while `v1.8` and `v2.x` do not. Report the\nverified version before D1. A missing, unparseable, wrong-major, or\nbelow-minimum version is `workflow_version_mismatch`: stop before\n`get_refill_target_plan` or mutation. Do NOT require exact string equality: the\nflow asset ships in the npm package while this skill is installed separately,\nso an exact pin makes every additive contract change a breaking one.\nRoute intent is immutable: ordinary refill uses `intent:\"auto\"` from first plan\nthrough terminal; explicit `yolo:false` uses the review-first path.\n\n## Mandatory visible plan before execution\n\nEvery refill in every mode begins with D1, a read-only\n`get_refill_target_plan` with the exact workspace, sender/action selectors, and\ndate envelope. For default yolo execution, the first D1 call must explicitly pass\n`approvalMode:\"approve\"`; never rely on the planner's `mark_ready` default and\nthen discover the scope mismatch at D2. Invoke D1 exactly once, retain and parse\nthat response in the same host composition, and never call D1 again only to\nformat or render it. Render its plan in normal chat before any mutation. At minimum,\nshow one row per selected sender/campaign lane with sender, campaign, lane/source\nfamily, planned first action, target/cap, sent, scheduled, ready, still needing\npreparation, fallback order, and blocker/skip reason. Then show the bounded side\neffects, forbidden actions, stop condition, `targetShapeRevision`, and first\n`globalActionQueue[0].actionKey`.\n\nIf a stale or missing paid-credit fact prevents a certified fillable cap, show\n`pending credit refresh`, never bare `unknown`. The target remains visible and\nyolo executes the displayed refresh action before replanning the numeric cap.\n\nAfter rendering D1, explicit `yolo:false` asks Accept or Decline and invokes\nonly the returned `planned_manual_run` after Accept. Default `yolo:true` does\nnot ask: call\n`refill_sends` D2 with `workspaceCoordinator:true`, the identical scope, and\nboth `expectedTargetShapeRevision` and `expectedActionKey` copied from the\ndisplayed packet. The server may itself return `status:\"plan_ready\"`; render\nthat returned plan before invoking `planned_manual_run` or\n`planned_workspace_run`. Thus the modes share the exact same visible plan and\nrevision fence. The only difference is that non-yolo pauses for decisions while\n`--yolo` auto-accepts the displayed packet and autonomously traverses every\nplanner-ranked existing campaign, lane, source, safe rubric trial, and scheduler\nhandoff until terminal. If either pin drifts, render the fresh read-only\nreplacement packet; do not mutate from the stale plan. No credit refresh, fence,\nor product mutation may exist before this render.\n\n## Coordinator lifecycle\n\n```mermaid\nflowchart TD\n A[\"Fresh read-only workspace refill plan rendered to the user\"] --> FULL{\"Projected coverage sent + scheduled is full?\"}\n FULL -- Yes --> COMPLETE[\"Terminal: complete\"]\n FULL -- No --> ACTIVE{\"Active exact fenced run exists?\"}\n ACTIVE -- Yes --> RESUME[\"Resume only issued runId, fence, targetConfig, reportingContext\"]\n ACTIVE -- No --> PICK[\"Select globalActionQueue 0\"]\n PICK --> PREFLIGHT[\"Exact campaign, table, sender, action, lane preflight\"]\n PREFLIGHT --> ACT[\"Execute one bounded planner action\"]\n RESUME --> ACT\n ACT --> OUTCOME{\"Canonical outcome?\"}\n OUTCOME -- \"Committed or canonical no-op\" --> A\n OUTCOME -- \"Still running\" --> WAIT[\"Bounded read-only wait or receipt reconciliation\"]\n WAIT --> A\n OUTCOME -- \"Source exhausted\" --> NEXT{\"Another planner-ranked existing target?\"}\n NEXT -- Yes --> HANDOFF[\"Terminalize exact fence as next_exact_target\"]\n HANDOFF --> A\n NEXT -- No --> NC[\"Terminal: new_campaign_required; ask whether to create it\"]\n OUTCOME -- \"Provider unavailable after one retry\" --> RETRY[\"Terminal: blocked_retryable; universeExhausted false\"]\n OUTCOME -- \"Scope, config, readiness, or receipt drift\" --> BLOCK[\"Terminal: blocked_retryable with exact blocker\"]\n OUTCOME -- \"Planner marker done\" --> DONE[\"Terminalize marker; never dispatch it\"]\n```\n\nIf `status:\"plan_ready\"`, render its full plan packet first. Non-yolo asks for\nAccept or Decline before `planned_manual_run`; `--yolo` invokes\n`planned_workspace_run` automatically. If `status:\"in_progress\"`, invoke only the returned\n`continuation.arguments`. `targetConfig` and `runHandle` are separate and remain\nunchanged except for a server-issued takeover fence. `active_exact_run` resumes\nthat fence; `next_exact_target` carries no stale run fence. Never open a second\nor overlapping run, ask the user to type “continue”, redispatch a control marker, or\nturn `done`, `next_campaign`, or a continuation object into a product action.\n\n## Per-sender campaign and lane waterfall\n\nUse managed evergreen order when healthy. Otherwise build the same waterfall\non the fly from existing eligible campaigns the sender is already attached to.\nThis makes regular and evergreen campaigns differ only in where ordering comes\nfrom, not in how refill proceeds.\n\n```mermaid\nflowchart TD\n A[\"All existing campaign-backed sequences attached to one sender\"] --> FILTER[\"Exclude wrong workspace/sender, archived, completed, direct, disconnected, or sequence-invalid targets\"]\n FILTER --> STALE{\"Managed waterfall complete and live?\"}\n STALE -- Yes --> MANAGED[\"Use configured priority order\"]\n STALE -- No --> DERIVE[\"Derive deterministic order from dashboard-active campaigns\"]\n DERIVE --> P1[\"Prefer canonical source/lane priority\"]\n P1 --> P2[\"Then future scheduled and ready inventory\"]\n P2 --> P3[\"Then recent successful sends and source health\"]\n P3 --> P4[\"Then active before exact start-eligible paused, recency, stable ID\"]\n MANAGED --> PIN[\"Pin exact per-sender and per-action lane chain for this run\"]\n P4 --> PIN\n PIN --> LANE[\"Select highest-priority non-exhausted lane\"]\n LANE --> REFILL[\"Run row lifecycle\"]\n REFILL --> EX{\"Lane structurally exhausted?\"}\n EX -- No --> REFILL\n EX -- Yes --> MORE{\"Another pinned existing lane or source family?\"}\n MORE -- Yes --> LANE\n MORE -- No --> END[\"Output: lanes_exhausted, then new_campaign_required if coverage still short\"]\n```\n\nCampaign enrollment/removal, missing pinned targets, or loss of eligibility is\ntyped scope drift; a fresh planner may rederive, but the host cannot silently\nsubstitute a target. A current dashboard-active `PAUSED` campaign is eligible\nonly when the planner names that exact campaign as start-eligible. Starting it\nmay let the product scheduler schedule/send approved sequence actions and must\nbe reported. DMs are follow-ups, not refill horizon targets. Mixed sequences\nkeep distinct exact target paths; do not choose campaign-union behavior.\n\n## Connection and InMail capacity overlay\n\n```mermaid\nflowchart TD\n S[\"Sender capacity and exact selected days\"] --> C{\"Connection slots available now or later in rolling week?\"}\n S --> F{\"Paid-InMail credit facts fresh?\"}\n F -- No --> RF[\"Refresh exact selected sender once per run, then replan\"]\n RF --> P{\"Credits meet existing threshold, normally 5?\"}\n F -- Yes --> P\n C -- Yes --> INV[\"Enable existing invite lanes\"]\n C -- No --> INVCAP[\"Cap invite lane with timing evidence\"]\n P -- Yes --> MAIL[\"Enable existing paid-InMail or cascade lane\"]\n P -- No --> MAILCAP[\"Skip paid lane; never lower threshold\"]\n INV --> UNION[\"Rank enabled existing lanes per sender\"]\n MAIL --> UNION\n INVCAP --> ALT{\"Paid lane enabled?\"}\n MAILCAP --> ALT2{\"Invite or same-campaign connection fallback enabled?\"}\n ALT -- Yes --> UNION\n ALT -- No --> CAP[\"Output: blocked_retryable / capacity or window\"]\n ALT2 -- Yes --> UNION\n ALT2 -- No --> CAP\n UNION --> POOL[\"Pool shared-campaign prepared inventory; never double-enrich\"]\n```\n\n`rollingWeeklyInvite.capacityFreedDuringWindow:true` means later capacity is\nschedulable; use its timing fields rather than treating the opening gate as a\nfull-day blocker. Credit freshness precedes scheduler wait. Fresh facts below\nthreshold authorize only an already-eligible existing connection fallback or\nanother existing campaign—not a threshold change or new campaign.\n\n## Row lifecycle\n\nFor every selected lane, use the smallest action that can reduce its gap.\n\n```mermaid\nflowchart TD\n A[\"Lane still has projected gap\"] --> ACTIVE{\"Active import, preparation, or stuck work?\"}\n ACTIVE -- \"Healthy active\" --> WAIT[\"Bounded read-only wait; honor wait.deadlineAt\"]\n WAIT --> A\n ACTIVE -- \"Stuck or anomalous\" --> REPAIR[\"Run only planner-bounded repairable cells or return blocker\"]\n REPAIR --> A\n ACTIVE -- No --> APPROVE{\"Generated rows can be approved?\"}\n APPROVE -- Yes --> AP[\"Approve exact bounded cohort with readiness authority\"]\n AP --> REPLAN[\"Record receipt and full authoritative replan\"]\n APPROVE -- No --> ENRICH{\"Existing rows can be enriched or prepared?\"}\n ENRICH -- Yes --> PREP[\"Enrich, generate, or rerun exact bounded cohort\"]\n PREP --> REPLAN\n ENRICH -- No --> SOURCE{\"Selected source can add qualified rows?\"}\n SOURCE -- Yes --> ADD[\"Copy/import bounded rows from exact selected source\"]\n ADD --> REPLAN\n SOURCE -- No --> EXPAND[\"Run source-family acquisition flow\"]\n EXPAND --> REPLAN\n REPLAN --> A\n```\n\nStructural exhaustion requires receipt-proven\n`hasMoreFrontierRows:false`, zero `approvalCandidates`, no fresh active prep,\nno `stuckActiveCells`, and no non-terminal `approvedNotDispatched` work. A dry\npreparation receipt is run-scoped and prevents the identical no-op from\nrepeating. `USER_ADDED_ROWS_LIMIT_EXCEEDED` may use a bounded same-source split\nthrough `load_csv_linkedin_leads` into the exact review batch; a true campaign\ntable hard cap returns a capacity blocker and never deletes rows.\n\n## Sender Post Engagers acquisition\n\n```mermaid\nflowchart TD\n A[\"Sender Post Engagers lane needs rows\"] --> UNUSED{\"Unused selected post/engager rows exist?\"}\n UNUSED -- Yes --> USE[\"Import/copy bounded unused rows\"]\n UNUSED -- No --> REFRESH[\"Refresh this sender's latest recent authored posts\"]\n REFRESH --> SELECT[\"Select new unprocessed relevant high-engagement posts\"]\n SELECT --> FETCH[\"Fetch/import their engagers and dedupe\"]\n FETCH --> YIELD{\"Usable qualified yield?\"}\n YIELD -- Yes --> USE\n YIELD -- No --> EX[\"Exhaust only this Post Engagers lane\"]\n EX --> NEXT[\"Advance to Shared Signal Discovery or next pinned existing lane\"]\n```\n\nThe campaign must be sender-owned: exactly that sender, authored posts only.\nNever invent keywords, search third-party posts, or run general Signal Discovery\nagainst the Post Engagers campaign.\n\n## Shared Signal Discovery acquisition\n\n```mermaid\nflowchart TD\n A[\"Shared Signal lane needs rows\"] --> UNUSED{\"Unused selected rows or unprocessed posts exist?\"}\n UNUSED -- Yes --> USE[\"Import/copy bounded rows\"]\n UNUSED -- No --> STALE[\"Re-search stale existing topics for newly published posts\"]\n STALE --> SY{\"Projected qualified yield meets min remaining gap or 100?\"}\n SY -- Yes --> USE\n SY -- No --> DERIVE[\"Derive up to five concise positive-ICP topics from approved campaign intent\"]\n DERIVE --> DY{\"Projected qualified yield meets floor?\"}\n DY -- Yes --> USE\n DY -- No --> AGENT[\"Request 3-5 new agent-supplied topics through the same fenced continuation\"]\n AGENT --> SEARCH[\"Search once, excluding searchedKeywords, then project actual returned candidates\"]\n SEARCH --> PROVIDER{\"All LinkedIn providers failed?\"}\n PROVIDER -- Yes --> RETRY[\"Retry the same bounded search once\"]\n RETRY --> RECOVER{\"Provider recovered?\"}\n RECOVER -- No --> BLOCK[\"Output: blocked_retryable / provider_unavailable; universeExhausted false\"]\n RECOVER -- Yes --> AY{\"Actual qualified yield meets floor?\"}\n PROVIDER -- No --> AY\n AY -- Yes --> USE\n AY -- No --> EX[\"Receipt-proven signal_yield_below_floor\"]\n EX --> NEXT[\"Advance to next pinned existing lane\"]\n```\n\nProvider outage never consumes a keyword tier or proves exhaustion. Temporary\nderived search state is run-scoped; later runs may rediscover new posts. Agent\nkeywords are supplied only through the exact run continuation, never by user\nsteering or a second run.\n\n## Sales Nav and Prospeo acquisition\n\n```mermaid\nflowchart TD\n A[\"Cold-provider lane needs rows\"] --> UNUSED{\"Unused exact-source rows exist?\"}\n UNUSED -- Yes --> USE[\"Import/copy bounded rows\"]\n UNUSED -- No --> DEEP[\"Continue latest/unfetched pages of exact saved Sales Nav or Prospeo search\"]\n DEEP --> DY{\"Qualified yield?\"}\n DY -- Yes --> USE\n DY -- No --> OPTIONAL{\"Known activity/recency filter exists?\"}\n OPTIONAL -- No --> NEXT[\"Advance to next pinned existing lane\"]\n OPTIONAL -- Yes --> CLONE[\"Clone exact approved search; remove one optional activity/recency filter\"]\n CLONE --> PROOF{\"Original unchanged and every hard-fit filter fingerprint preserved?\"}\n PROOF -- No --> BLOCK[\"Output: blocked_retryable / provider_search_drift\"]\n PROOF -- Yes --> TRIAL[\"Import bounded trial candidates into existing gated source\"]\n TRIAL --> GATE[\"Run current ICP, exclusion, DNC, persona, geography, and company-fit gates\"]\n GATE --> GY{\"Qualified yield?\"}\n GY -- Yes --> USE\n GY -- No --> QUARANTINE[\"Rejects remain unsendable; advance lane\"]\n QUARANTINE --> NEXT\n```\n\nThe original saved search is immutable. An automatic trial may remove only a\nknown activity/recency signal; role, seniority, geography, industry, company\nsize, domain, include/exclude, DNC, and every other hard-fit constraint remain\nbyte-for-byte. Imported candidates are not approvals: current campaign gates\nquarantine bad fits. If rubric rejects dominate — including a receipt-proven\n`low_yield` cohort below the 10% planning floor, not only zero-prepared — enter\nthe bounded sample-quality trial below before another source add. Do not repeat\na spent trial or switch outside planner order.\n\n## Provider prompt before any new provider search\n\nDraining an already-imported source list needs no provider context. But before\nany action that runs a NEW provider search — a `broaden_signal_search` round\n(especially when supplying `agentKeywords`), a Sales Nav or Prospeo cloned-\nsearch broadening, or any source add that dispatches a fresh search — load the\nmatching specialized prompt first with\n`get_provider_prompt({ provider, campaignOfferId })` and apply its discipline:\nsample-first fit checks against the headline ICP criteria, the 10% planning\nfloor, sample math (target ÷ observed pass rate) for sizing, and the keyword-\nquality guidance for Signal Discovery. Inventing broaden keywords or sizing a\nscrape without the provider prompt in context repeats the blind-import failure\nthe sampling discipline exists to prevent. The search tools also enforce this\npreflight server-side; loading it late wastes a fenced round on a typed refusal.\n\n## Rubric sample-quality trial\n\nRubric rejection may mean the source is poor or that one or several required\nchecks are unnecessarily narrow. Decide from an exact sample, not pass-rate alone.\n\n```mermaid\nflowchart TD\n A[\"Rows fail ICP/rubric gate\"] --> STALE{\"Score stale, errored, or computed under mismatched authority?\"}\n STALE -- Yes --> RESCORE[\"Rerun exact bounded cohort under unchanged current rubric\"]\n RESCORE --> RESULT{\"Now qualified?\"}\n RESULT -- Yes --> CONTINUE[\"Continue row lifecycle\"]\n RESULT -- No --> SAMPLE\n STALE -- No --> SAMPLE[\"Take exact representative rejected sample from prep receipt\"]\n SAMPLE --> SNAP[\"Snapshot complete rubric and prior digest\"]\n SNAP --> PROPOSE[\"Propose smallest 1-3 required-to-advisory bundle that creates useful new passes\"]\n PROPOSE --> HARD{\"Touches employment, DNC/opt-out, legal/compliance, or explicit exclusion?\"}\n HARD -- Yes --> REJECT[\"Reject proposal; preserve complete prior rubric\"]\n HARD -- No --> SIM[\"Simulate bundle from recorded per-prospect evaluations; no mutation\"]\n SIM --> DELTA{\"Any newly passing prospects?\"}\n DELTA -- No --> REJECT\n DELTA -- Yes --> QUALITY[\"Review only newly passing profiles in campaign and customer context\"]\n QUALITY --> REPLY{\"If these prospects replied, would the customer be comfortable and consider the replies relevant?\"}\n REPLY -- No --> REJECT\n REPLY -- Yes --> DRIFT{\"Live rubric digest still equals prior digest?\"}\n DRIFT -- No --> BLOCK[\"Output: blocked_retryable / rubric config drift\"]\n DRIFT -- Yes --> APPLY[\"Guarded apply entire required-to-advisory bundle\"]\n APPLY --> EXACT[\"Force exact bounded ICP rescore for accepted sample\"]\n EXACT --> VERIFY{\"Dispatch and accepted quality receipt valid?\"}\n VERIFY -- Yes --> KEEP[\"Keep bundle; receipt before/after digests, changed checks, sample counts, and verdict\"]\n VERIFY -- No --> ROLLBACK[\"Restore complete prior snapshot and receipt rollback\"]\n REJECT --> BETTER[\"Keep rows unsendable; continue better supply or next existing lane\"]\n ROLLBACK --> BETTER\n```\n\n`--yolo` may change more than one required rubric at a time only through this\nsingle bounded reversible bundle. The change demotes selected required checks to\nadvisory scoring; it does not erase their evidence. The acceptance test is the\nnewly passing delta: if any reviewed prospect would make the customer unhappy,\nsurprised, or consider the reply irrelevant, keep the complete prior rubric and\ncontinue the source waterfall. Never expose raw prospect profiles in the public\nreceipt.\n\n## Scheduler and uncertain-receipt recovery\n\n```mermaid\nflowchart TD\n A[\"Ready buffer covers remaining projected gap\"] --> CREDIT{\"Every selected paid lane has fresh credit facts?\"}\n CREDIT -- No --> RF[\"Refresh each exact selected sender once, then full replan\"]\n RF --> A\n CREDIT -- Yes --> DATE{\"Exact targetDate?\"}\n DATE -- Yes --> SWEEP[\"Dispatch one request-scoped product scheduler sweep\"]\n DATE -- No --> WAIT[\"Enter coordinator-owned bounded scheduler wait\"]\n SWEEP --> RECEIPT{\"Receipt state?\"}\n RECEIPT -- Terminal --> READ[\"Canonical target-plan and request/effect readback\"]\n RECEIPT -- Active --> POLL[\"Return in_progress and poll only the same request key\"]\n RECEIPT -- uncertain_outcome --> RECON[\"Reconcile same request key read-only; never redispatch\"]\n POLL --> RECEIPT\n RECON --> TERM[\"Terminal: blocked_retryable with request identity intact\"]\n WAIT --> READ\n READ --> FULL{\"Projected coverage full?\"}\n FULL -- Yes --> COMPLETE[\"Terminal: complete\"]\n FULL -- No --> READY{\"Ready buffer still covers gap?\"}\n READY -- Yes --> OTHER{\"Any other workspace target still needs preparation or an executable gate?\"}\n OTHER -- Yes --> PREP[\"Continue next planner-ranked exact target\"]\n OTHER -- No --> BLOCK[\"Terminal: receipt-backed blocked_retryable scheduler gap\"]\n READY -- No --> PREP[\"Return to row lifecycle\"]\n```\n\nA scheduler sweep is a visible workspace-wide scheduling side effect: existing\nproduct gates may schedule unrelated eligible cells in that workspace/date.\nIt never sends directly or raw-writes scheduler fields. `cellsConsidered is\nallocation-attempt count`; `readyCellsFound` is prefilter inventory. Inspect\n`campaignScopeSummary`, `prefiltered`, `skipped`, and `deferred`. For ready\nclosed-InMail cells with stale paid-credit evidence, use\n`refresh_paid_inmail_credits_then_rerun`. `wait_for_capacity_or_window` means do\nnot source or prep more rows; `no_ready_cells_continue_refill_prep` returns to\nthe row lifecycle. `cellsScheduled:0` alone is not failure. Once a sweep is\ndispatched, a host wait budget yields an `in_progress` continuation carrying\nthe same run identity. Keep polling that request key until its official\nterminal receipt, then reread the full target plan. Never convert an active\nsweep into `loaded_awaiting_scheduler`.\n\n## Explicit message-template revision\n\nOrdinary refill never changes copy. Only a literal user-approved revision\nenvelope may enter this side path.\n\n```mermaid\nflowchart TD\n A[\"Literal messageTemplateRevision with approved markdown, digests, 1-500 row IDs, request/effect IDs\"] --> DIGEST{\"Current prior authority digest present?\"}\n DIGEST -- No --> FENCE[\"Acquire exact run and read reportingContext source templateAuthorityDigest\"]\n FENCE --> DG{\"Digest returned?\"}\n DG -- No --> BLOCK[\"Terminal: blocked_retryable / template_authority_missing\"]\n DG -- Yes --> REVISE[\"Apply revision to exact cohort\"]\n DIGEST -- Yes --> REVISE\n REVISE --> AUDIT[\"select_campaign_cells in batches <=20 with includeGeneratedMessageReview:true\"]\n AUDIT --> MATCH{\"Every authoritativeGeneratedMessageReview complete, exact text/digest, matchesCurrentTemplateAuthority true?\"}\n MATCH -- No --> FAIL[\"Stop before approval: regenerated_copy_integrity_failed\"]\n MATCH -- Yes --> APPROVE[\"Approve only exact rowIds using compiled readiness authority\"]\n APPROVE --> RECEIPT{\"Bounded preparation receipt includes laneScope?\"}\n RECEIPT -- No --> FAIL\n RECEIPT -- Yes --> PLAN[\"Return to canonical refill plan\"]\n```\n\nFormula Message/Subject columns, previews, exports, and counts are not copy\nauthority. `forceRerun:true` never implies a revision or approval.\n\n## Manual approval and `--yolo`\n\n```mermaid\nflowchart TD\n A[\"Render fresh bounded packet in normal chat as Markdown\"] --> MODE{\"--yolo present?\"}\n MODE -- Yes --> AUTO[\"Auto-accept all planner-ranked transitions among existing campaigns, lanes, sources, and safe refill primitives\"]\n MODE -- No --> ASK[\"Open host-native structured question with exactly Accept and Decline\"]\n ASK --> DECISION{\"Operator choice?\"}\n DECISION -- Accept --> RUN[\"Execute only rendered envelope\"]\n DECISION -- Decline --> STOP[\"Stop with no mutation\"]\n AUTO --> RUN\n```\n\nUse `plain chat` in Codex and `plain chat` in Claude Code. The\nfull packet belongs in the normal chat message immediately before the question:\nworkspace, sender scope, campaign-by-campaign plan table, action, target/cap,\nsource, blocker/skip reason, exact IDs, expected side effects, forbidden actions,\nand stop condition. The question body must be compact and refer back to the\nposted packet instead of duplicating it. `--yolo` may switch among every fresh\nplanner-ranked existing campaign/lane/source family and start the exact selected\n`PAUSED` campaign. It does not create campaigns, direct-send, archive/delete,\nchange sender limits or paid-InMail thresholds, reassign senders, or invent\nsource selection outside the planner.\n\n## Typed outputs\n\n```mermaid\nflowchart TD\n A[\"Fresh canonical state\"] --> C{\"Terminal classification\"}\n C -- \"sent + scheduled meets target\" --> COMPLETE[\"complete: report saturation ledger\"]\n C -- \"all remaining exact targets scheduler-owned and workspace ready-or-projected gap is zero\" --> LOADED[\"loaded_awaiting_scheduler: report expected pickup and stop\"]\n C -- \"recoverable external, capacity, active-work, or receipt condition\" --> BLOCKED[\"blocked_retryable: exact blocker, receipt, and safe retry condition\"]\n C -- \"current lane spent but another existing target remains\" --> EX[\"lanes_exhausted internally; continue next_exact_target\"]\n C -- \"every existing campaign/lane/source spent or infeasible\" --> NEW[\"new_campaign_required: propose missing shape and ask whether to create it\"]\n C -- \"scope/config/readiness changed\" --> DRIFT[\"blocked_retryable: typed drift; never guess\"]\n```\n\nOne scheduler-loaded exact lane is not a workspace terminal: skip it and\ncontinue other planner-ranked lanes. Workspace `loaded_awaiting_scheduler` is\nadmissible only when no refill-owned scheduler request is active. After refill\ndispatches a sweep, keep the same fence alive until the official terminal\nreceipt; then return `complete` from the post-receipt readback or a concrete\nreceipt-backed retryable scheduler blocker if coverage remains short. The sole\nrefill-ladder expansion\nthat asks the user in `--yolo` is `new_campaign_required`; refill itself never\ncreates that campaign. `new_campaign_required` is also the truthful terminal\nwhen receipts prove every existing lane is spent but the planner has no\nautomatic cold rung to propose (Signal Discovery workspaces): report the\nremaining gap and ask the user; a future handler may automate the creation. A\n`actionable_supply_not_queued` terminal means actionable row frontiers\n(approval/generate/enrich/rubric or unprocessed frontier rows) still exist but\nno executable exact edge surfaced: retry once with a fresh command, and if it\nrepeats report it as a planner-exposure defect instead of treating the\nworkspace as exhausted. That census counts CONVERTIBLE rows only, de-duplicated\nper campaign/table lane — a frontier whose own preparation receipt carries a\nnon-retryable `icp_or_rubric_rejection` diagnosis is proven unable to convert,\nso it never inflates the census. When such receipts are all that remain, the\nterminal is `source_supply_exhausted_at_rubric`: a truthful source exhaustion,\nnot a defect and not retryable. Report the per-lane evidence from the note and\ntell the user that widening the rubric or adding fresh source rows is the only\nremaining fill path. Whenever a terminal reports a non-empty census, it also\nnames every exact edge the run refused (`Exact edges this run refused: ...`) —\nsurface those named blockers rather than describing the workspace as having no\navailable work.\n\n## Non-negotiable authority and proof\n\n- Before preparation, approval, or exact-date scheduling, require a validated\n compiled campaign and positive exact target readiness identity from the\n backend compiler: `profileSchemaVersion`, `compilerVersion`,\n `campaignDigest`, `actionColumnId`, action, `pathDigest`,\n `dependsOnColumnIds`, `runCondition`, the selected target's `capabilities`,\n `requestId`, `effectId`, and `dateGuards`. Never infer no-message behavior\n from a missing Generate Message column. Prepare rows according to the\n persisted sequence; bounded enrichment and LLM prerequisite calls are\n allowed only when authorized by the exact readiness packet and receipted.\n- Refuse approval outside the bounded authorized cohort. The exact selected\n target path controls message, subject, comment, reaction, and row approval.\n For exact dates, require the date strictly after sender-local today and inside\n the fill horizon. Prepared, approved, and ready rows remain intermediate\n evidence; completion requires request/effect-attributed scheduler-owned\n scheduled readback.\n- Execute one bounded primitive, then perform a full authoritative reread.\n Never call a LinkedIn outreach/provider-send surface from refill. No direct\n sends, raw scheduler writes, broad approval, threshold/limit changes,\n sender reassignment, destructive cleanup, or new campaign creation.\n- Maintain the target-window saturation ledger per sender: selected days,\n gross capacity, actual sent, future scheduler-owned scheduled with non-null\n `scheduledFor`, projected coverage (`sent + scheduled`), ready buffer,\n remaining gap, paid-InMail feasibility, `targetShapeRevision`, and\n `stateRevision`. Future scheduled coverage and already sent actions are\n distinct.\n- Carry canonical `refill_reporting.v2` unchanged through progress,\n continuation, replay, and terminal output. Completion proof is Sellable MCP\n evidence only: target plan, campaign refill state, scheduler capacity,\n sweep/status, and bounded receipts. Never use individual cell ids, Prisma,\n SQL, direct database access, or production-environment scripts as completion\n proof. Redact raw copy and prospect fields.\n\n## Refill V3\n\nThis is the DEFAULT route whenever `refill_v3_advance` is exposed (see\n[Route selection](#route-selection--do-this-first)). `refill_v3_advance` is then\nthe only execution owner and `refill-sends-workflow` plus its `core/flow.v3.json`\nasset is the contract to load — load the prompt and that asset, verify the `v3`\ncompatible range, and report the version before the first call. Start from the\nbase request `{workspaceId, scope}`, add `yolo:true` by default, and call it\nagain after each `advanced` or\nsettled `campaign_attention_required` result. A `run_step_pending` result means\nthe server is still completing the already-consumed step: wait its\n`retryAfterMs`, then call advance with the same public inputs. The MCP retains\nthe identical token internally; never copy a token, route to AI, or execute an\nauthority for this transport-reconciliation wait. `refill_v3_continue` itself\nsettles the exact packet action before it returns. Bounded enrichment and an\nalready-active preparation job use the shared Create Campaign waiter and\ndurable preparation owner; directly queued row repair uses the campaign waiter.\nSynchronous source, configuration, rubric, and exhaustion authorities already\nreturn after their own exact reread, so they return `settlement.ready:true` from\nthat bounded authority receipt and never wait on unrelated campaign-wide\nprocessing cells.\n\nWhen an explicit `yolo:false` request names a campaign, resolve that visible name with one\nauthorized bounded `get_campaigns` call carrying an explicit `limit`. Require\nexactly one case-insensitive exact match; if none or more than one match, stop\nwithout preview or mutation. Retain only the returned stable campaign ID. Call\n`refill_v3_world_state({workspaceId, scope, campaignId})`, render that exact\ncampaign's two-lane readiness and its explicit zero product side effects, then\nstop at the product-native question with exactly Accept and Decline. Do not call\n`refill_v3_advance` before Accept. Accept calls\n`refill_v3_advance({workspaceId, scope, campaignId})`; Decline stops. Carry the\nsame campaignId through every advance, continue, replay, partial-wait, and\npost-wait repeat. Display-name changes never alter this retained identity, and\nno sibling campaign may replace it.\nCall the next advance only when `settlement.ready` is true. When it is false,\ncall `wait_for_campaign_processing` with `settlement.resumeInput` **unchanged**;\nthat carries the exact `preparationJobId`, `requirePreparationTerminal:true`,\nand `requireIdle:true`. Repeat the returned `resumeInput` until ready; never\nfall back to a stats-only campaign wait for this settlement. An\n`awaiting_external_change` result with\n`deferral.code:\"campaign_work_in_flight\"` and a campaign id is a bounded\nread-only continuation, not permission to walk the next campaign: call\n`wait_for_campaign_processing({ workspaceId, campaignId, requireIdle:true })`,\nthen call `refill_v3_advance` again with the identical scope and yolo grant.\nThis reuses Create Campaign's campaign-table waiter and guarantees the same\ncampaign is freshly replanned after its cohort settles. If the bounded wait\nreturns a partial timeout, surface that checkpoint and make the fresh advance;\nif it reports the same live campaign, repeat the bounded wait. Stop on every\nother `awaiting_external_change`, `complete`, or `blocked` result. For each of\nthose terminal results, report **every** entry in `terminalEvidence.rows`, one\nper scoped sender/date/lane—not only the headline deferral or blocker. Include\n`targetSlots`, `takenSlots`, `remainingTargetGap`, `fillableSlotsNow`,\n`blockedGap`, `terminalClassification`, `reasonCodes`, `approvedDelta`, and\n`scheduledDelta`. When `reasonCodes` contains\n`paid_inmail_credit_refresh_failed`, also report `paidCreditRefreshAttempt`\nexactly: `attemptedAt`, `outcome`, `errorClass`, `errorDetail`, and `durationMs`.\nA cumulative observation, vague \"credit refresh\" label, or one surfaced blocker\nis never a row-complete terminal summary.\n\n`terminalEvidence.rows` covers exactly two Refill V3 lanes:\n`connection_invite` and closed/paid InMail. It does **not** report Open InMail.\nCampaign `supply.readyToSchedule` is a broad first-touch table count and can\ninclude Open InMail rows that the normal scheduler can place independently.\nNever present that broad count as connection or paid inventory, never infer\nOpen InMail is empty or unscheduled from a Refill V3 row, and never explain the\ndifference as sender affinity unless exact lane evidence proves it. If asked\nwhether Open InMail is maxed, say the two-lane refill receipt cannot answer that\nquestion and obtain an action-type-specific scheduled readback before answering.\n\nLead every row-complete terminal summary by stating that all scoped\nsender/date/lane rows were explored. Never say the workspace \"stopped on\" one\nsender or headline blocker: that blocker classifies one row after independent\nsibling work was explored; it is not the traversal stop.\n\nResolve the workspace BEFORE the first call, exactly as the V1 route already\nrequires. `workspaceId` must be an exact id on every automation call: when the\noperator names a workspace, call `list_workspaces` first and match that name to\nits id. Prefer one case-insensitive exact name match; if none exists, accept one\ncase-insensitive prefix match (so `Damiano` resolves `Damiano R`). Use the\nconfigured or active workspace only when the operator named none. If zero or\nmultiple prefix matches remain, stop and say so — do not fall back to the\nconfigured workspace — and never change the shared active workspace to steer\nan automation.\n\nThe command-shaped form is unambiguous: in `refill sends <name> [flags]`,\n`<name>` is the workspace override, never a sender name. A sender restriction\nmust be explicit through `--sender`/`senderIds`/`senderNames` or prose that says\n“sender.” Therefore call `list_workspaces` and resolve `<name>` first. Do not\nlist configured-workspace senders before that workspace match; doing so is both\nscope drift and avoidable latency.\n\nMap the rest of the request onto those keys and nothing else: the sender/date\nenvelope becomes `scope`. `sender_local_horizon` (1-3 days) is ONLY for\nrequests with no date control at all (\"refill sends X\"); the moment the\noperator names any date — `--target-date`, `--until-date`, or dated prose —\nthe scope is `exact_sender_dates`, on the FIRST call and every later call of\nthe run. There is no mode: WHICH campaign may be started is DERIVED from the\nworkspace's own campaigns and can never be requested.\n\nA DATE CONTROL always maps to `exact_sender_dates`, derived like this and never\napproximated with a horizon:\n\n- `--target-date D`: `dates: [D]` for every resolved sender.\n- `--until-date U` (sender-local, INCLUSIVE): for every resolved sender, list\n EVERY sender-local calendar date from that sender's local today through `U`,\n in order, with `U` itself present. From a local Saturday the 2nd with\n `--until-date` the 5th that is `[\"2026-08-02\",\"2026-08-03\",\"2026-08-04\",\n\"2026-08-05\"]` — four dates, not a 3-day window. Dropping the boundary date\n or substituting today+2 silently changes which days the run is accountable\n for; the backend types today's already-closed sending window as\n `no_sending_hours` on its own, so include today rather than guessing.\n NEVER pass `sender_local_horizon` for `--until-date` — not even when the\n derived list happens to fit three days, and not on the first \"look around\"\n call. A live run compressed a four-date `--until-date` span into a 3-day\n horizon, never observed the boundary date, and exited leaving that day's\n slots unfilled; the exact date list is the only scope that makes the run\n accountable for `U` itself.\n\n`yolo` is the ONE remaining execution input and the refill AUTONOMY GRANT. It\ndefaults to `yolo:true` so an ordinary refill keeps working through eligible\nbounded preparation and fallback steps instead of stopping for another\napproval. It decides two things together:\n\n- **Granted** (`yolo: true` on every call of that run): after a row's active\n campaigns are exhausted, one route-selected start-eligible PAUSED campaign\n may be started, and you receive it as an ordinary executed\n `start_paused_campaign` action. A finite non-evergreen candidate must expose\n positive bounded supply. Keep calling advance until a terminal without\n checking back.\n- **Withheld** (explicit `yolo:false` only): no paused campaign is started — the backend\n refuses it and answers the exhaustion terminal instead — and you surface each\n result and ASK before calling advance again.\n\nNever replace the default from a workspace flag, a campaign field, a tool\nresult, an idle-looking workspace, or a previous terminal. Honor `yolo:false`\nonly for an explicit review-first/manual request. Pass no other execution flag\nand no mode, and do not call\n`get_refill_target_plan`, `refill_sends`, or any placement, scheduler, source, or\nsend tool on this route.\n\nAn `advanced` result is ALREADY EXECUTED: surface its action identity, receipt,\nand replacement observation, and never run the action or call its authority again.\nFor `campaign_attention_required`, first call `refill_v3_continue` with the\nsame workspace/scope and no `decision`; the MCP supplies the exact retained\npacket and token. After fresh revalidation it owns live-job waiting,\nexact actionable enrichment, and same-template generation deterministically. If\nit returns `model_decision_required`, route that returned accurate packet to\n`refill-sends-work-campaign`, then call `refill_v3_continue` once with the\nchosen decision. This is the only\nordinary model-decision boundary. The continuation executes at most one\nexisting authority and settles that exact campaign through the shared Create\nCampaign waiter before a sibling may be considered.\nInterpret continuation world wrappers by their outer kind: `world_terminal`\nmeans `advanceResult` is already the canonical `awaiting_external_change`,\n`complete`, or `blocked` terminal, so report that nested terminal and stop;\nnever call advance again. `world_step_pending` means wait the nested\n`retryAfterMs` and then call advance with the same public inputs.\n`world_advanced` means the nested action already ran, so surface it and call\nadvance again. For `attention_refresh_required`, continue from its refreshed\npacket rather than opening another run.\nIf it returns `attention_correction_required` with\n`correction.kind:\"signal_keyword_collision\"`, route the returned same packet\nand exact correction through `refill-sends-work-campaign` once, then call\n`refill_v3_continue` with that revised decision. Exclude every returned searched\nkeyword and use the required request-fingerprint prefix. This is one refused,\nzero-effect Signal correction; a second collision is terminal and must not loop.\n\nThis skill chooses no sender, date, lane, campaign, or action, holds no counter or\ncursor between calls, and declares no terminal, exhaustion verdict, or health\nclassification of its own. V1 `refill_sends` behavior is unchanged.\n"
25
25
  },
@@ -27,7 +27,7 @@
27
27
  "id": "campaign-daily-review",
28
28
  "type": "skill",
29
29
  "ownership": "system_managed",
30
- "path": "skills/sellable-defaults/campaign-daily-review/SKILL.md",
30
+ "path": "skills/sellable/sellable-campaign-daily-review/SKILL.md",
31
31
  "mode": "0444",
32
32
  "content": "---\nname: sellable-campaign-daily-review\ndescription: Review existing Sellable campaigns and move exactly one useful next step forward.\nvisibility: public\nallowed-tools:\n - mcp_sellable_get_auth_status\n - mcp_sellable_customer_program\n - mcp_sellable_get_campaigns\n - mcp_sellable_get_campaign\n - mcp_sellable_get_campaign_context\n - mcp_sellable_get_campaign_refill_state\n - mcp_sellable_refill_sends\n---\n\n# Sellable Campaign Daily Review\n\n## Installed Host Contract\n\nThis installed skill is running in Hermes Agent. When the shared workflow body\nor fallback text mentions Claude Code, Codex, or Hermes for internal parity,\nchoose the Hermes instruction for customer-facing language and host functions.\n\n- Customer-facing command: `/sellable-campaign-daily-review`\n- MCP tool naming: Hermes exposes Sellable tools as `mcp_sellable_<tool>`;\n when shared instructions show `mcp_sellable_<tool>`, call the matching\n `mcp_sellable_<tool>` tool instead.\n- Structured questions: ask plainly in chat unless a Hermes-native approval or\n question tool is visible in the current session.\n- Bootstrap host label: `host: \"Hermes\"`\n- Install/reload blocker label: Hermes install/reload problem\n- Reload instruction: restart Hermes, or run `/reload-mcp` in the active\n Hermes session after install\n\nDo not tell Hermes users to run Codex or Claude command forms, use Codex/Claude structured-question APIs, or restart Codex Desktop or Claude Code. Do not describe this run as Claude Code or Codex.\n\nHelp the customer keep the campaign they already have healthy and moving. This\nis one continuous workflow: understand current workspace state, choose one\nuseful next step, use the existing campaign owner, observe the result, and save\nthe exact wait or outcome.\n\n## Run the review\n\n1. Call `get_auth_status`, then obtain a fenced `customer_program` review claim\n and load the complete home-workspace campaign inventory. For a scheduled\n `execute` or `resume` wake, use the scheduled trigger and the wake gate's\n exact lease owner. For a direct customer request, use the interactive\n trigger. For a `reconcile` wake, skip campaign work and follow the delivery\n recovery below. Keep each strict tool input separate: claim with only\n `action`, `trigger`, and `leaseOwner`; never add the wake's `effectId` or\n `fence` to the claim call.\n2. Infer the relationship stage from that workspace state. Keep campaign health\n and wait state separate from the relationship stage.\n3. Assess each campaign from approved ICP, source, message and actual outcome\n evidence. `ACTIVE` or a full queue does not prove suitability. Sparse results\n do not prove poor quality. Unavailable data is not an empty workspace.\n4. Inspect supply and capacity, then choose exactly one outcome: `maintain`,\n `refill`, `continue`, `edit`, `propose_create`, `input`, or `blocker`.\n5. Execute through the existing owner:\n - suitable campaign with a real supply gap: call canonical Refill Sends with\n `yolo:true` and keep its returned run token;\n - unfinished campaign: continue the same campaign through Create Campaign;\n - supported quality issue: edit the same campaign through Create Campaign;\n - no campaign: enter Create Campaign, asking only for the LinkedIn profile\n first when it is missing;\n - healthy supplied campaign: maintain it and stay quiet;\n - manual pause: respect it;\n - missing approval, connection or decision: retain one exact next step.\n6. Observe the owned operation, then record the outcome against the same review\n effect and fence. The `record_review_outcome` call contains only its action,\n effect, fence, review outcome, summary, and optional decision/operation refs;\n do not include trigger or lease fields. Never mark a pending or uncertain\n effect complete. On a scheduled actionable outcome, saving the outcome is\n not completion. Inspect `deliveryRequired`; when it is `true`, continue\n immediately to the required `stage_delivery` call. Never return while the\n recorded run still has `deliveryState: \"pending\"`.\n\n## Stop and delivery rules\n\nStop on settled state, a human wait, a retained async wait, a typed blocker or\nthe existing bounded operation budget. Never restart a pending effect. Follow a\nnew customer reply or product completion immediately instead of waiting for the\nnext daily tick.\n\nPost a useful new decision or blocker once to the verified current main channel.\nRoutine refill belongs in the next results brief. A healthy no-op is inspectable\nand silent. On a scheduled actionable outcome, compose the complete final Slack\ntext, call `customer_program` with only `action: \"stage_delivery\"`, the exact\neffect and fence, and `text`, then inspect `delivery.deliverNow`. Only when it is `true`, return\n`delivery.text` verbatim as the native job's final response. When it is\n`false`, return `[SILENT]`; that effect is already uncertain or delivered,\nand the managed reconciliation tick owns the next step. Do not stage an interactive response.\n\nBefore any scheduled final response, check the recorded run one last time. A\n`pending` delivery is an unfinished cron turn: call `stage_delivery` now. Only\n`accepted_uncertain`, `delivered`, `failed`, or `not_applicable` may end the\nturn.\n\nOn a scheduled `reconcile` wake, call `reconcile_delivery` with only its action\nand the exact effect.\nIf trusted Slack history confirms the text, or retry is not yet allowed, return\n`[SILENT]`. If retry is allowed, call `retry_delivery` with the current fence and\nreturn its stored `delivery.text` verbatim. Never regenerate it. The native\nmanaged job remains the only Slack sender; the product owns the claim and\ndelivery ledger. After three unconfirmed attempts the product closes the run as\nfailed and later ticks stay silent. Never create another post path or fall back\nto another channel.\n\nDo not create a second campaign engine, refill algorithm, scheduler, scoring\nframework, intake questionnaire or generic morning approval. Do not rewrite\nstrategy or messages merely because another day passed. Initial campaign launch\nstill requires the existing human Start.\n"
33
33
  },
@@ -35,7 +35,7 @@
35
35
  "id": "campaign-daily-results",
36
36
  "type": "skill",
37
37
  "ownership": "system_managed",
38
- "path": "skills/sellable-defaults/campaign-daily-results/SKILL.md",
38
+ "path": "skills/sellable/sellable-campaign-daily-results/SKILL.md",
39
39
  "mode": "0444",
40
40
  "content": "---\nname: sellable-campaign-daily-results\ndescription: Show one honest snapshot of yesterday's Sellable campaign results and the best-fit people who accepted or replied.\nvisibility: public\nallowed-tools:\n - mcp_sellable_get_auth_status\n - mcp_sellable_customer_program\n---\n\n# Sellable Campaign Daily Results\n\n## Installed Host Contract\n\nThis installed skill is running in Hermes Agent. When the shared workflow body\nor fallback text mentions Claude Code, Codex, or Hermes for internal parity,\nchoose the Hermes instruction for customer-facing language and host functions.\n\n- Customer-facing command: `/sellable-campaign-daily-results`\n- MCP tool naming: Hermes exposes Sellable tools as `mcp_sellable_<tool>`;\n when shared instructions show `mcp_sellable_<tool>`, call the matching\n `mcp_sellable_<tool>` tool instead.\n- Structured questions: ask plainly in chat unless a Hermes-native approval or\n question tool is visible in the current session.\n- Bootstrap host label: `host: \"Hermes\"`\n- Install/reload blocker label: Hermes install/reload problem\n- Reload instruction: restart Hermes, or run `/reload-mcp` in the active\n Hermes session after install\n\nDo not tell Hermes users to run Codex or Claude command forms, use Codex/Claude structured-question APIs, or restart Codex Desktop or Claude Code. Do not describe this run as Claude Code or Codex.\n\nGive the customer one concise snapshot of the prior complete day from their\nexisting campaigns. This is a read-only results workflow. It does not create,\nedit, launch or refill campaigns, and it does not send or draft replies.\n\n## Read the snapshot\n\n1. Call `get_auth_status`, then call `customer_program` with\n `action: \"daily_results\"` and `trigger: \"interactive\"`. For the managed\n `execute` or `resume` wake, use `trigger: \"scheduled\"` and the wake gate's\n exact lease owner. This strict call contains only `action`, `trigger`, and\n `leaseOwner`; never add the wake's `effectId` or `fence`. For a `reconcile`\n wake, skip snapshot generation and follow delivery recovery below.\n2. Use the returned period label and timezone exactly. Describe it as the prior\n complete local day. Keep today's morning work separate from yesterday's\n outcomes, and label pending work as pending.\n3. Report the returned counts for invitations actually delivered, invitations\n accepted, and unique people who replied. Keep interest and booked meetings\n separate. Render unavailable values as unknown with the returned reason;\n never turn missing evidence into zero.\n4. Show at most five returned people. Each person must retain the campaign ICP\n evidence, profile or inbox link, acceptance/reply facts, outcome label and\n current handling state supplied by the product. Say whether the customer has\n replied, a draft is saved, a follow-up is scheduled, or attention is still\n needed. Do not invent fit, interest, meetings or draft state.\n5. End with the single returned next action when present. If the active program\n had a quiet day, give the brief zero-results snapshot without commentary. If\n there is no campaign, do not show an empty scoreboard; offer to start or\n continue Create Campaign instead.\n\n## Stop and delivery rules\n\nA direct request returns the snapshot in the current conversation without\nclaiming scheduled publication. The managed 09:00 run posts only when the\nproduct returns a new publication claim, and only to the verified current main\nchannel. A muted report, pre-campaign workspace, duplicate period or replay is\nsilent. For a scheduled publication, compose the complete final Slack text,\ncall `customer_program` with only `action: \"stage_delivery\"`, the exact effect,\nfence, and `text`, then inspect `delivery.deliverNow`. Only when it is `true`, return\n`delivery.text` verbatim as the native job's final response. When it is `false`,\nreturn `[SILENT]`; that effect is already uncertain or delivered, and the\nmanaged reconciliation tick owns the next step.\n\nOn a scheduled `reconcile` wake, call `reconcile_delivery` with only its action\nand the exact effect.\nIf trusted Slack history confirms the text, or retry is not yet allowed, return\n`[SILENT]`. If retry is allowed, call `retry_delivery` with the current fence and\nreturn its stored `delivery.text` verbatim. Never regenerate it. The native\nmanaged job remains the only Slack sender; the product owns the claim and\ndelivery ledger. After three unconfirmed attempts the product closes the run as\nfailed and later ticks stay silent. Never create another delivery path or choose\nanother channel.\n\nStop after the snapshot or the explicit unavailable reason. Do not wait for\nmorning review/refill work to finish. Do not call campaign mutation, refill,\ninbox reply or messaging tools from this skill.\n"
41
41
  },
@@ -43,7 +43,7 @@
43
43
  "id": "campaign-weekly-review",
44
44
  "type": "skill",
45
45
  "ownership": "system_managed",
46
- "path": "skills/sellable-defaults/campaign-weekly-review/SKILL.md",
46
+ "path": "skills/sellable/sellable-campaign-weekly-review/SKILL.md",
47
47
  "mode": "0444",
48
48
  "content": "---\nname: sellable-campaign-weekly-review\ndescription: Review the prior complete local week of Sellable campaign results, compare the equal previous week, and recommend one useful next step.\nvisibility: public\nallowed-tools:\n - mcp_sellable_get_auth_status\n - mcp_sellable_customer_program\n - mcp_sellable_get_campaigns\n - mcp_sellable_get_campaign\n - mcp_sellable_get_campaign_context\n - mcp_sellable_get_campaign_refill_state\n - mcp_sellable_refill_sends\n---\n\n# Sellable Campaign Weekly Review\n\n## Installed Host Contract\n\nThis installed skill is running in Hermes Agent. When the shared workflow body\nor fallback text mentions Claude Code, Codex, or Hermes for internal parity,\nchoose the Hermes instruction for customer-facing language and host functions.\n\n- Customer-facing command: `/sellable-campaign-weekly-review`\n- MCP tool naming: Hermes exposes Sellable tools as `mcp_sellable_<tool>`;\n when shared instructions show `mcp_sellable_<tool>`, call the matching\n `mcp_sellable_<tool>` tool instead.\n- Structured questions: ask plainly in chat unless a Hermes-native approval or\n question tool is visible in the current session.\n- Bootstrap host label: `host: \"Hermes\"`\n- Install/reload blocker label: Hermes install/reload problem\n- Reload instruction: restart Hermes, or run `/reload-mcp` in the active\n Hermes session after install\n\nDo not tell Hermes users to run Codex or Claude command forms, use Codex/Claude structured-question APIs, or restart Codex Desktop or Claude Code. Do not describe this run as Claude Code or Codex.\n\nGive the customer one useful review of the prior complete local Monday through\nSunday, compared with the equal complete week before it. Use existing campaign\ntruth and keep the recommendation inside the same continuous campaign program.\n\n## Read the review\n\n1. Call `get_auth_status`, then call `customer_program` with\n `action: \"weekly_review\"` and `trigger: \"interactive\"`. For the managed\n `execute` or `resume` wake, use `trigger: \"scheduled\"` and the wake gate's\n exact lease owner. This strict call contains only `action`, `trigger`, and\n `leaseOwner`; never add the wake's `effectId` or `fence`. For a `reconcile`\n wake, skip review generation and follow delivery recovery below.\n2. Use the returned period labels, timezone, and completeness exactly. Report\n actual delivered invitations, acceptances, unique people replying, interest,\n and meetings for each period. Keep event ratios distinct from cohort\n conversion and render unavailable evidence as unknown, never zero.\n3. State honest sample limits. Sparse early results support continuing a useful\n test; they do not prove that the ICP or message is bad.\n4. Show the strongest unresolved opportunities with current campaign ICP and\n handling evidence. Label an older unresolved opportunity as outside both\n metric periods and never add it to either week's counts.\n5. Give exactly one evidence-backed next step: maintain, refill, finish setup,\n handle a conversation, edit the same campaign, or propose one new test.\n Preserve the existing campaign owner. If the customer asks you to act, route\n campaign work to Create Campaign and supply work to canonical Refill Sends\n with `yolo:true`; do not mutate strategy inside this report.\n6. On Monday, include the returned prior complete local-day snapshot in the one\n combined message. On a later recovery wake, use the returned latest complete\n local day. Omit a section the product marks already delivered.\n\n## Stop and delivery rules\n\nA direct request returns the review in the current conversation. A scheduled\nclaim posts at most one combined top-level message to the verified current main\nchannel. Respect daily and weekly mutes independently. If both sections are\nmuted, the week is a quiet healthy no-op, or the period was already delivered,\nstay silent. If a section is unavailable, say so once; do not create an edit or\nwaiting coordinator. For a scheduled publication, compose the complete final\nSlack text, call `customer_program` with only `action: \"stage_delivery\"`, the exact\neffect, fence, and `text`, then inspect `delivery.deliverNow`. Only when it is `true`, return\n`delivery.text` verbatim as the native job's final response. When it is\n`false`, return `[SILENT]`; that effect is already uncertain or delivered,\nand the managed reconciliation tick owns the next step.\n\nOn a scheduled `reconcile` wake, call `reconcile_delivery` with only its action\nand the exact effect.\nIf trusted Slack history confirms the text, or retry is not yet allowed, return\n`[SILENT]`. If retry is allowed, call `retry_delivery` with the current fence and\nreturn its stored `delivery.text` verbatim. Never regenerate it. The native\nmanaged job remains the only Slack sender; the product owns the claim and\ndelivery ledger. After three unconfirmed attempts the product closes the run as\nfailed and later ticks stay silent. Never create another post path.\n\nStop after the review and one recommendation. Do not create another campaign\nengine, score, questionnaire, approval loop, scheduler, child cron, or direct\nplacement path. Do not launch a campaign; initial launch remains the existing\nhuman Start.\n"
49
49
  }
@@ -37,6 +37,15 @@ const ADMIN_MATERIALIZER_ANCHOR = "skills/sellable-admin/SKILL.md";
37
37
  const ADMIN_NAMESPACE_RECEIPT_SCHEMA =
38
38
  "sellable-admin-skill-namespace-receipt/v1";
39
39
 
40
+ const legacyManagedSkillPath = (entry) =>
41
+ `skills/sellable-defaults/${entry.id}/SKILL.md`;
42
+ const canonicalPublicSkillPath = (entry) =>
43
+ `skills/sellable/${entry.nativeName}/SKILL.md`;
44
+ const acceptedSkillPath = (entry) =>
45
+ entry.path === legacyManagedSkillPath(entry) ||
46
+ (entry.nativeName === `sellable-${entry.id}` &&
47
+ entry.path === canonicalPublicSkillPath(entry));
48
+
40
49
  const sha256 = (value) => createHash("sha256").update(value).digest("hex");
41
50
  const canonicalJson = (value) => {
42
51
  if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
@@ -348,16 +357,16 @@ function readBundle(bundleRoot) {
348
357
  }
349
358
  const files = manifest.files.map((entry) => {
350
359
  const expectedPath =
351
- entry?.type === "skill"
352
- ? `skills/sellable-defaults/${entry.id}/SKILL.md`
353
- : `crons/${entry?.name}.json`;
360
+ entry?.type === "cron" ? `crons/${entry?.name}.json` : null;
354
361
  if (
355
362
  !MANAGED_ID.test(entry?.id ?? "") ||
356
363
  !["skill", "cron"].includes(entry.type) ||
357
364
  entry.ownership !== "system_managed" ||
358
365
  !["SHARED", "CUSTOMER", "ADMIN", "FIXTURE"].includes(entry.kind) ||
359
366
  entry.mode !== "0444" ||
360
- entry.path !== expectedPath ||
367
+ (entry.type === "skill"
368
+ ? !acceptedSkillPath(entry)
369
+ : entry.path !== expectedPath) ||
361
370
  !SHA256.test(entry.sha256 ?? "") ||
362
371
  (entry.type === "skill" && !MANAGED_ID.test(entry.nativeName ?? "")) ||
363
372
  (entry.type === "cron" &&
@@ -542,19 +551,22 @@ export async function reconcileDefaultProfileBundle({
542
551
  cronSpecs.set(entry.id, cronDesired(entry));
543
552
  continue;
544
553
  }
545
- const directory = join(
546
- profileRoot,
547
- "skills",
548
- "sellable-defaults",
549
- entry.id
550
- );
551
- const path = join(directory, "SKILL.md");
554
+ const path = join(profileRoot, ...entry.path.split("/"));
555
+ const directory = dirname(path);
556
+ const publicDirectory =
557
+ entry.path === canonicalPublicSkillPath(entry);
552
558
  if (existsSync(directory)) {
553
559
  const stat = lstatSync(directory);
554
560
  if (stat.isSymbolicLink() || !stat.isDirectory())
555
561
  throw new Error("default_profile_skill_collision");
556
562
  for (const name of readdirSync(directory)) {
557
563
  if (name === "SKILL.md") continue;
564
+ if (publicDirectory && name === "SOUL.md") {
565
+ const soul = lstatSync(join(directory, name));
566
+ if (soul.isSymbolicLink() || !soul.isFile() || soul.nlink !== 1)
567
+ throw new Error("default_profile_skill_collision");
568
+ continue;
569
+ }
558
570
  const stage = join(directory, name);
559
571
  const stageStat = lstatSync(stage);
560
572
  if (
@@ -568,14 +580,68 @@ export async function reconcileDefaultProfileBundle({
568
580
  rmSync(stage);
569
581
  }
570
582
  }
583
+ const priorEntry = historical(entry);
584
+ const legacyPath = legacyManagedSkillPath(entry);
585
+ const priorPath =
586
+ priorEntry?.path ?? (priorEntry ? legacyPath : null);
587
+ let legacyDirectory = null;
588
+ if (
589
+ priorPath !== null &&
590
+ priorPath !== entry.path &&
591
+ priorPath !== legacyPath
592
+ ) {
593
+ throw new Error("default_profile_receipt_rejected");
594
+ }
595
+ if (priorPath === legacyPath && legacyPath !== entry.path) {
596
+ const legacySkillPath = join(profileRoot, ...legacyPath.split("/"));
597
+ const candidateLegacyDirectory = dirname(legacySkillPath);
598
+ if (existsSync(candidateLegacyDirectory)) {
599
+ const stat = lstatSync(candidateLegacyDirectory);
600
+ if (stat.isSymbolicLink() || !stat.isDirectory())
601
+ throw new Error("default_profile_skill_collision");
602
+ for (const name of readdirSync(candidateLegacyDirectory)) {
603
+ if (name === "SKILL.md") continue;
604
+ const stage = join(candidateLegacyDirectory, name);
605
+ const stageStat = lstatSync(stage);
606
+ if (
607
+ !name.startsWith(STAGE_PREFIX) ||
608
+ stageStat.isSymbolicLink() ||
609
+ !stageStat.isFile() ||
610
+ stageStat.nlink !== 1
611
+ ) {
612
+ throw new Error("default_profile_skill_collision");
613
+ }
614
+ rmSync(stage);
615
+ }
616
+ if (
617
+ !existsSync(legacySkillPath) ||
618
+ sha256(readRegular(legacySkillPath, 64 * 1024)) !==
619
+ priorEntry.sha256
620
+ ) {
621
+ throw new Error("default_profile_skill_collision");
622
+ }
623
+ legacyDirectory = candidateLegacyDirectory;
624
+ }
625
+ }
571
626
  let status = "CREATED";
627
+ let write = true;
572
628
  if (existsSync(path)) {
573
629
  const current = sha256(readRegular(path, 64 * 1024));
574
- if (current === entry.sha256) status = "REUSED";
575
- else if (historical(entry)?.sha256 === current) status = "UPDATED";
630
+ if (current === entry.sha256) {
631
+ status = "REUSED";
632
+ write = false;
633
+ } else if (priorEntry?.sha256 === current) status = "UPDATED";
576
634
  else throw new Error("default_profile_skill_collision");
577
635
  }
578
- skillPlans.set(entry.id, { directory, path, status });
636
+ if (priorPath !== null && priorPath !== entry.path)
637
+ status = "UPDATED";
638
+ skillPlans.set(entry.id, {
639
+ directory,
640
+ path,
641
+ status,
642
+ write,
643
+ legacyDirectory,
644
+ });
579
645
  }
580
646
 
581
647
  const actions = [];
@@ -583,12 +649,14 @@ export async function reconcileDefaultProfileBundle({
583
649
  for (const entry of entries) {
584
650
  if (entry.type === "skill") {
585
651
  const plan = skillPlans.get(entry.id);
586
- if (plan.status !== "REUSED") {
652
+ if (plan.write) {
587
653
  mkdirSync(plan.directory, { recursive: true, mode: 0o700 });
588
654
  atomicWrite(plan.path, entry.content, 0o444);
589
- if (sha256(readRegular(plan.path, 64 * 1024)) !== entry.sha256)
590
- throw new Error("default_profile_skill_readback_rejected");
591
655
  }
656
+ if (sha256(readRegular(plan.path, 64 * 1024)) !== entry.sha256)
657
+ throw new Error("default_profile_skill_readback_rejected");
658
+ if (plan.legacyDirectory)
659
+ rmSync(plan.legacyDirectory, { recursive: true, force: true });
592
660
  actions.push({
593
661
  type: "skill",
594
662
  id: entry.id,
@@ -598,6 +666,7 @@ export async function reconcileDefaultProfileBundle({
598
666
  receiptEntries.push({
599
667
  type: "skill",
600
668
  id: entry.id,
669
+ path: entry.path,
601
670
  sha256: entry.sha256,
602
671
  });
603
672
  continue;
@@ -42,8 +42,8 @@ ARG PLAYWRIGHT_BROWSER_REVISION=1234
42
42
  ARG STRIPE_CLI_PACKAGE=@stripe/cli-linux-x64@1.45.0
43
43
  ARG STRIPE_CLI_INTEGRITY=sha512-1ZhoPpoweYfynqsvhCLlTjZ7lPz5IKtxqAMT5MBy6zrwLRTP9x0T6r8h/jLYL8lRH1c7zC/6X6/XUu2V+r9Bog==
44
44
  ARG DEFAULT_PROFILE_BUNDLE_FIXTURE_VERSION=1
45
- ARG DEFAULT_PROFILE_BUNDLE_VERSION=3
46
- ARG DEFAULT_PROFILE_BUNDLE_DIGEST=197fb4ed5a60cd1a3a06a46aa1fc53a04bb1e6c1f872e96a0ce2cc28bf2856de
45
+ ARG DEFAULT_PROFILE_BUNDLE_VERSION=4
46
+ ARG DEFAULT_PROFILE_BUNDLE_DIGEST=03b32cafd87030f57831c28646b7b010abe664dfbe8fbc078631493acf44c481
47
47
 
48
48
  COPY --from=slack-pp-cli-builder --chmod=0555 /out/slack-pp-cli /usr/local/bin/slack-pp-cli
49
49
 
@@ -8,7 +8,7 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright
8
8
 
9
9
  ARG MCP_PACKAGE=@sellable/mcp@0.1.938
10
10
  ARG MCP_INTEGRITY=sha512-wcVCiCXVvKeE/SufP+2iXk8zgzwYWh78M/1vwa7oorF9UZbzut9szyZV47t4iMX9owYgBVaGwdlOC6eX8VQX4A==
11
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.751
11
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.754
12
12
  ARG ADMIN_MCP_PACKAGE=@sellable/admin-mcp@0.1.202
13
13
  ARG ADMIN_MCP_INTEGRITY=sha512-ym3aye/0vQ8tfULJlKz9JFOxvIQsl1G+n3np+TTgK/tecwNblBo/j3AkTnRqH9iomKzPbZzP+n4tEQsvsU8ByA==
14
14
  ARG TUS_JS_CLIENT_VERSION=4.3.1
@@ -20,8 +20,8 @@ ARG PLAYWRIGHT_VERSION=1.62.1
20
20
  ARG PLAYWRIGHT_BROWSER_REVISION=1234
21
21
  ARG TIRITH_ARCHIVE_SHA256=6cdbe35e8f9ccf42e70ad95b501c93cd218ac18201c3df958d54f6ba0d995ce2
22
22
  ARG DEFAULT_PROFILE_BUNDLE_FIXTURE_VERSION=1
23
- ARG DEFAULT_PROFILE_BUNDLE_VERSION=3
24
- ARG DEFAULT_PROFILE_BUNDLE_DIGEST=301ed49582dbfb29453ff2c2fa26579da6771d19f58e12218c5400db2e14318d
23
+ ARG DEFAULT_PROFILE_BUNDLE_VERSION=4
24
+ ARG DEFAULT_PROFILE_BUNDLE_DIGEST=51e22479fa00de9a178e92bbac743448372797c84ab07faa8b70230396c07342
25
25
 
26
26
  RUN --mount=type=secret,id=mcp_trust_root,required=true \
27
27
  test -s /run/secrets/mcp_trust_root \
@@ -121,7 +121,7 @@ const RUNTIME_GENERATION = Number(
121
121
  const BOOT_SESSION_ID = randomUUID();
122
122
  const HERMES_VERSION = process.env.SELLABLE_AGENT_HERMES_VERSION ?? "0.21.3";
123
123
  const INSTALLER_PACKAGE =
124
- process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.751";
124
+ process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.754";
125
125
  const MCP_PACKAGE =
126
126
  process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.938";
127
127
  export const ENDPOINT_MAX_CONCURRENT_RUNS = 10;
@@ -38,7 +38,7 @@ import { installAgentHostServices } from "./service-installer.mjs";
38
38
  const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
39
39
  const VERSION = "sellable-agent-host-bootstrap/v1";
40
40
  const RECEIPT_VERSION = "sellable-agent-host-registration/v1";
41
- const INSTALLER_PACKAGE = "@sellable/install@0.1.751";
41
+ const INSTALLER_PACKAGE = "@sellable/install@0.1.754";
42
42
  const MCP_PACKAGE = "@sellable/mcp@0.1.938";
43
43
  const HERMES_VERSION = "0.18.0";
44
44
  const SHA256 = /^[a-f0-9]{64}$/;
@@ -1546,7 +1546,7 @@ export function compileClaimToProfileDesired(activeClaim, config) {
1546
1546
  ]) ||
1547
1547
  pinned.hermesCli !== "hermes" ||
1548
1548
  pinned.hermesVersion !== "0.18.0" ||
1549
- pinned.installerPackage !== "@sellable/install@0.1.751" ||
1549
+ pinned.installerPackage !== "@sellable/install@0.1.754" ||
1550
1550
  pinned.mcpPackage !== "@sellable/mcp@0.1.938" ||
1551
1551
  !Array.isArray(policy.toolInclude) ||
1552
1552
  policy.toolInclude.length === 0 ||
@@ -282,7 +282,7 @@ function validateDesired(desired) {
282
282
  desired.serviceCredentialGeneration < 1 ||
283
283
  desired.hermesCli !== "hermes" ||
284
284
  desired.hermesVersion !== "0.18.0" ||
285
- desired.installerPackage !== "@sellable/install@0.1.751" ||
285
+ desired.installerPackage !== "@sellable/install@0.1.754" ||
286
286
  desired.mcpPackage !== "@sellable/mcp@0.1.938" ||
287
287
  !Array.isArray(desired.toolInclude) ||
288
288
  desired.toolInclude.length === 0 ||
@@ -25,7 +25,7 @@ import { fileURLToPath } from "node:url";
25
25
  import { deriveContainedProfileId } from "./profile-materializer.mjs";
26
26
 
27
27
  export const PROVISIONING_ACTION = "PROVISION_HERMES_PROFILE";
28
- export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.751";
28
+ export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.754";
29
29
  export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.938";
30
30
  const PINNED_INSTALL_VERSION = PINNED_INSTALL_PACKAGE.slice(
31
31
  PINNED_INSTALL_PACKAGE.lastIndexOf("@") + 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.752",
3
+ "version": "0.1.754",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {