@sellable/install 0.1.320 → 0.1.322

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.
@@ -22,6 +22,14 @@ import {
22
22
  REQUIRED_SELLABLE_MCP_TOOLS,
23
23
  verifySellableMcpRuntime,
24
24
  } from "../lib/runtime-verify.mjs";
25
+ import {
26
+ activateCodexCandidateSelection,
27
+ installCodexCandidateCache,
28
+ readCodexPluginSelection,
29
+ resolveCodexPluginSelectionPaths,
30
+ restoreCodexPluginSelection,
31
+ snapshotCodexPluginSelection,
32
+ } from "../lib/codex-plugin-selection.mjs";
25
33
 
26
34
  const DEFAULT_API_URL = "https://app.sellable.dev";
27
35
  const DEFAULT_SERVER_PACKAGE =
@@ -40,7 +48,7 @@ function getInstallVersion() {
40
48
  }
41
49
  }
42
50
 
43
- const CODEX_PLUGIN_VERSION = "0.1.54";
51
+ const CODEX_PLUGIN_VERSION = "0.1.56";
44
52
  const CODEX_PLUGIN_COMPAT_VERSIONS = [
45
53
  "0.1.8",
46
54
  "0.1.9",
@@ -79,6 +87,8 @@ const CODEX_PLUGIN_COMPAT_VERSIONS = [
79
87
  "0.1.51",
80
88
  "0.1.52",
81
89
  "0.1.53",
90
+ "0.1.54",
91
+ "0.1.55",
82
92
  ];
83
93
  const RAW_INSTALL_PACKAGE_SPEC =
84
94
  process.env.SELLABLE_INSTALL_PACKAGE_SPEC || "@sellable/install@latest";
@@ -170,6 +180,8 @@ Commands:
170
180
  Save the ask-first prompt-sharing preference.
171
181
  setup claude-permissions --allow-common-setup
172
182
  Add common Sellable Bash permissions to Claude settings.
183
+ codex plugin-selection snapshot|install-cache|activate|restore|status
184
+ Approval-gated Phase 98 exact-cache selection controls.
173
185
  hermes profile bootstrap Create a profile-local Hermes config, Sellable auth config,
174
186
  configs dir, skills, and optional Slack .env.
175
187
  uninstall Remove Sellable host config and installed artifacts.
@@ -2038,6 +2050,20 @@ then retry \`get_subskill_prompt\`.
2038
2050
  `, host, "create-evergreen-campaigns");
2039
2051
  }
2040
2052
 
2053
+ const REFILL_SENDS_FALLBACK_CONTRACT = `<!-- REFILL_CONTRACT_GENERATED:START -->
2054
+ contractVersion: 2.0.0
2055
+ cacheVersion: refill-contract-v1
2056
+ contractHash: e54ba0f80bc534fd70fd8c10c1caab109fe9a91c205cfc423f5f6735cd53fdc1
2057
+ scope: workspace_id, target_date, sender_selectors, campaign_selectors, approval_mode, contract_identity, installed_identity
2058
+ gates: approval_packet, one_primitive, fresh_reread, normalized_accounting, prep_circuit
2059
+ forbidden: direct_send, raw_scheduler_write, campaign_create, campaign_archive_delete, threshold_lowering, unselected_source_mutation, unapproved_campaign_start, sender_config_write
2060
+ terminal: projected_full, concrete_blocker, deadline_reached, iteration_limit, operator_stopped
2061
+ ready_buffer_exists_is_not_complete
2062
+ mcpPackageVersion: installed MCP package spec in host MCP config
2063
+ installPackageVersion: ${getInstallVersion()}
2064
+ codexPluginVersion: ${CODEX_PLUGIN_VERSION}
2065
+ <!-- REFILL_CONTRACT_GENERATED:END -->`;
2066
+
2041
2067
  function refillSendsSkillMd(host = "shared") {
2042
2068
  try {
2043
2069
  const here = dirname(fileURLToPath(import.meta.url));
@@ -2047,7 +2073,10 @@ function refillSendsSkillMd(host = "shared") {
2047
2073
  "skill-templates",
2048
2074
  "refill-sends.md"
2049
2075
  );
2050
- if (existsSync(templatePath)) {
2076
+ if (
2077
+ process.env.SELLABLE_FORCE_REFILL_TEMPLATE_MISSING !== "1" &&
2078
+ existsSync(templatePath)
2079
+ ) {
2051
2080
  return stampInstalledHost(
2052
2081
  addOrReplaceAllowedTools(
2053
2082
  readFileSync(templatePath, "utf8"),
@@ -2070,6 +2099,14 @@ ${allowedToolsYaml(REFILL_SENDS_ALLOWED_TOOLS)}
2070
2099
 
2071
2100
  # Sellable Refill Sends
2072
2101
 
2102
+ ${REFILL_SENDS_FALLBACK_CONTRACT}
2103
+
2104
+ fallbackSource: embedded
2105
+
2106
+ Load \`refill-sends-workflow\`, then \`core/flow.v1.json\`, then
2107
+ \`core/contract.v2.json\`, each through the Sellable MCP prompt tools until
2108
+ \`hasMore:false\`. Fail closed on unsupported major or stale contract hash.
2109
+
2073
2110
  Use this as the customer-facing entrypoint for the Sellable \`refill-sends\`
2074
2111
  workflow. It supports \`--yolo\` and optional repeated \`--sender <name-or-id>\`
2075
2112
  selectors. It also supports \`--target-date YYYY-MM-DD\` to fill one exact
@@ -5813,6 +5850,73 @@ async function main() {
5813
5850
  printCreateCommandHint();
5814
5851
  process.exit(0);
5815
5852
  }
5853
+ if (rawArgs[0] === "codex" && rawArgs[1] === "plugin-selection") {
5854
+ const command = rawArgs[2];
5855
+ const flags = new Map();
5856
+ for (let index = 3; index < rawArgs.length; index += 2) {
5857
+ const flag = rawArgs[index];
5858
+ const value = rawArgs[index + 1];
5859
+ if (!flag?.startsWith("--") || !value || value.startsWith("--")) {
5860
+ throw new Error(`Invalid Codex plugin-selection option: ${flag ?? "<missing>"}`);
5861
+ }
5862
+ flags.set(flag, value);
5863
+ }
5864
+ const required = (name) => {
5865
+ const value = flags.get(name);
5866
+ if (!value) throw new Error(`Missing value for ${name}`);
5867
+ return value;
5868
+ };
5869
+ const paths = resolveCodexPluginSelectionPaths({ home: homedir() });
5870
+ let result;
5871
+ if (command === "status") {
5872
+ result = readCodexPluginSelection({ paths, stableVersion: flags.get("--stable-version") });
5873
+ } else if (command === "snapshot") {
5874
+ result = snapshotCodexPluginSelection({
5875
+ paths,
5876
+ snapshotRoot: required("--snapshot-root"),
5877
+ });
5878
+ } else if (command === "install-cache") {
5879
+ result = installCodexCandidateCache({
5880
+ paths,
5881
+ sourcePluginRoot: required("--source"),
5882
+ version: required("--version"),
5883
+ });
5884
+ } else if (command === "activate") {
5885
+ result = activateCodexCandidateSelection({
5886
+ paths,
5887
+ snapshotManifestPath: required("--snapshot"),
5888
+ expectedSnapshotHash: required("--snapshot-hash"),
5889
+ candidateVersion: required("--candidate-version"),
5890
+ expectedCandidateHash: required("--candidate-hash"),
5891
+ approvalId: required("--approval-id"),
5892
+ approvedAt: required("--approved-at"),
5893
+ restoreBy: required("--restore-by"),
5894
+ now: required("--now"),
5895
+ });
5896
+ } else if (command === "restore") {
5897
+ result = restoreCodexPluginSelection({
5898
+ paths,
5899
+ snapshotManifestPath: required("--snapshot"),
5900
+ expectedSnapshotHash: required("--snapshot-hash"),
5901
+ activationPath: required("--activation"),
5902
+ now: required("--now"),
5903
+ recoveryReason: flags.get("--recovery-reason") || undefined,
5904
+ taskEvidence:
5905
+ flags.has("--task-id") && flags.has("--resolved-candidate-hash")
5906
+ ? {
5907
+ taskId: flags.get("--task-id"),
5908
+ resolvedCandidateHash: flags.get("--resolved-candidate-hash"),
5909
+ }
5910
+ : undefined,
5911
+ });
5912
+ } else {
5913
+ throw new Error(
5914
+ "Usage: sellable codex plugin-selection snapshot|install-cache|activate|restore|status"
5915
+ );
5916
+ }
5917
+ console.log(JSON.stringify(result, null, 2));
5918
+ process.exit(0);
5919
+ }
5816
5920
  if (
5817
5921
  rawArgs[0] === "hermes" &&
5818
5922
  rawArgs[1] === "profile" &&
@@ -0,0 +1,341 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ closeSync,
4
+ cpSync,
5
+ existsSync,
6
+ lstatSync,
7
+ mkdirSync,
8
+ openSync,
9
+ readFileSync,
10
+ readlinkSync,
11
+ readdirSync,
12
+ renameSync,
13
+ rmSync,
14
+ symlinkSync,
15
+ writeFileSync,
16
+ } from "node:fs";
17
+ import { basename, dirname, join, relative, resolve } from "node:path";
18
+
19
+ export const CODEX_PLUGIN_SELECTION_SCHEMA_VERSION = 1;
20
+ export const MAX_CODEX_CANARY_WINDOW_MS = 90 * 60 * 1000;
21
+
22
+ export function resolveCodexPluginSelectionPaths({
23
+ home = "",
24
+ marketplaceRoot = join(home, ".sellable", "codex-marketplace"),
25
+ cacheRoot = join(home, ".codex", "plugins", "cache", "sellable", "sellable"),
26
+ } = {}) {
27
+ if (!home) throw new Error("codex_selection_home_required");
28
+ return {
29
+ home: resolve(home),
30
+ configPath: join(home, ".codex", "config.toml"),
31
+ marketplaceRoot: resolve(marketplaceRoot),
32
+ marketplacePath: join(marketplaceRoot, ".agents", "plugins", "marketplace.json"),
33
+ pluginRoot: join(marketplaceRoot, "plugins", "sellable"),
34
+ cacheRoot: resolve(cacheRoot),
35
+ };
36
+ }
37
+
38
+ function canonicalize(value) {
39
+ if (Array.isArray(value)) return value.map(canonicalize);
40
+ if (value && typeof value === "object") {
41
+ return Object.fromEntries(
42
+ Object.keys(value)
43
+ .sort()
44
+ .map((key) => [key, canonicalize(value[key])])
45
+ );
46
+ }
47
+ return value;
48
+ }
49
+
50
+ function digest(value) {
51
+ return createHash("sha256").update(value).digest("hex");
52
+ }
53
+
54
+ function canonicalDigest(value) {
55
+ return digest(JSON.stringify(canonicalize(value)));
56
+ }
57
+
58
+ function treeRecords(root) {
59
+ if (!existsSync(root)) return [{ path: ".", type: "missing" }];
60
+ const records = [];
61
+ const visit = (path) => {
62
+ const stat = lstatSync(path);
63
+ const rel = relative(root, path) || ".";
64
+ if (stat.isSymbolicLink()) {
65
+ records.push({ path: rel, type: "symlink", target: readlinkSync(path) });
66
+ return;
67
+ }
68
+ if (stat.isDirectory()) {
69
+ records.push({ path: rel, type: "directory", mode: stat.mode & 0o777 });
70
+ for (const name of readdirSync(path).sort()) visit(join(path, name));
71
+ return;
72
+ }
73
+ records.push({
74
+ path: rel,
75
+ type: "file",
76
+ mode: stat.mode & 0o777,
77
+ sha256: digest(readFileSync(path)),
78
+ });
79
+ };
80
+ visit(root);
81
+ return records;
82
+ }
83
+
84
+ function treeHash(root) {
85
+ return canonicalDigest(treeRecords(root));
86
+ }
87
+
88
+ function pluginVersion(pluginRoot) {
89
+ const manifestPath = join(pluginRoot, ".codex-plugin", "plugin.json");
90
+ if (!existsSync(manifestPath)) throw new Error("codex_selection_plugin_manifest_missing");
91
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
92
+ if (manifest.name !== "sellable" || typeof manifest.version !== "string") {
93
+ throw new Error("codex_selection_plugin_manifest_invalid");
94
+ }
95
+ return manifest.version;
96
+ }
97
+
98
+ function ensureInside(parent, child, code) {
99
+ const root = resolve(parent);
100
+ const target = resolve(child);
101
+ if (!(target === root || target.startsWith(`${root}/`))) throw new Error(code);
102
+ }
103
+
104
+ function copyExact(source, target) {
105
+ rmSync(target, { recursive: true, force: true });
106
+ mkdirSync(dirname(target), { recursive: true });
107
+ cpSync(source, target, {
108
+ recursive: true,
109
+ dereference: false,
110
+ preserveTimestamps: true,
111
+ });
112
+ }
113
+
114
+ function readCanonicalManifest(manifestPath, expectedSnapshotHash) {
115
+ const raw = readFileSync(manifestPath, "utf8");
116
+ const manifest = JSON.parse(raw);
117
+ if (raw !== `${JSON.stringify(manifest, null, 2)}\n`) {
118
+ throw new Error("codex_selection_snapshot_manifest_not_canonical");
119
+ }
120
+ const { snapshotHash, ...unsigned } = manifest;
121
+ if (snapshotHash !== canonicalDigest(unsigned) || snapshotHash !== expectedSnapshotHash) {
122
+ throw new Error("codex_selection_snapshot_hash_mismatch");
123
+ }
124
+ for (const item of manifest.items ?? []) {
125
+ if (treeHash(item.backupPath) !== item.hash) {
126
+ throw new Error(`codex_selection_snapshot_backup_mismatch:${item.name}`);
127
+ }
128
+ }
129
+ return manifest;
130
+ }
131
+
132
+ function stateItems(paths, stableVersion) {
133
+ return [
134
+ { name: "config", sourcePath: paths.configPath },
135
+ { name: "marketplace", sourcePath: paths.marketplacePath },
136
+ { name: "pluginRoot", sourcePath: paths.pluginRoot },
137
+ { name: "stableCache", sourcePath: join(paths.cacheRoot, stableVersion) },
138
+ ];
139
+ }
140
+
141
+ function stateHash(paths, stableVersion) {
142
+ return canonicalDigest(
143
+ stateItems(paths, stableVersion).map((item) => ({
144
+ name: item.name,
145
+ sourcePath: item.sourcePath,
146
+ hash: treeHash(item.sourcePath),
147
+ }))
148
+ );
149
+ }
150
+
151
+ export function snapshotCodexPluginSelection({ paths, snapshotRoot }) {
152
+ if (!paths || !snapshotRoot) throw new Error("codex_selection_snapshot_arguments_required");
153
+ if (existsSync(snapshotRoot)) throw new Error("codex_selection_snapshot_root_exists");
154
+ const version = pluginVersion(paths.pluginRoot);
155
+ const items = stateItems(paths, version).map((item) => {
156
+ if (!existsSync(item.sourcePath)) {
157
+ throw new Error(`codex_selection_snapshot_source_missing:${item.name}`);
158
+ }
159
+ const backupPath = join(snapshotRoot, "data", item.name);
160
+ copyExact(item.sourcePath, backupPath);
161
+ return { ...item, backupPath, hash: treeHash(item.sourcePath) };
162
+ });
163
+ const unsigned = {
164
+ schemaVersion: CODEX_PLUGIN_SELECTION_SCHEMA_VERSION,
165
+ createdAt: new Date().toISOString(),
166
+ pluginVersion: version,
167
+ stateHash: stateHash(paths, version),
168
+ paths,
169
+ items,
170
+ };
171
+ const manifest = { ...unsigned, snapshotHash: canonicalDigest(unsigned) };
172
+ const manifestPath = join(snapshotRoot, "snapshot.json");
173
+ mkdirSync(dirname(manifestPath), { recursive: true });
174
+ writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
175
+ return { manifestPath, snapshotHash: manifest.snapshotHash, pluginVersion: version };
176
+ }
177
+
178
+ export function installCodexCandidateCache({ paths, sourcePluginRoot, version }) {
179
+ if (!paths || !sourcePluginRoot || !version) throw new Error("codex_candidate_cache_arguments_required");
180
+ if (pluginVersion(sourcePluginRoot) !== version) throw new Error("codex_candidate_version_mismatch");
181
+ const target = join(paths.cacheRoot, version);
182
+ ensureInside(paths.cacheRoot, target, "codex_candidate_cache_path_escape");
183
+ const sourceHash = treeHash(sourcePluginRoot);
184
+ if (existsSync(target)) {
185
+ if (treeHash(target) !== sourceHash) throw new Error("codex_candidate_cache_collision");
186
+ } else {
187
+ mkdirSync(paths.cacheRoot, { recursive: true });
188
+ const staging = join(paths.cacheRoot, `.phase98-${version}-${process.pid}`);
189
+ copyExact(sourcePluginRoot, staging);
190
+ if (treeHash(staging) !== sourceHash) throw new Error("codex_candidate_cache_copy_mismatch");
191
+ renameSync(staging, target);
192
+ }
193
+ return { version, cachePath: target, cacheHash: sourceHash };
194
+ }
195
+
196
+ function withSelectionLock(paths, fn) {
197
+ mkdirSync(paths.cacheRoot, { recursive: true });
198
+ const lockPath = join(paths.cacheRoot, ".phase98-selection.lock");
199
+ let handle;
200
+ try {
201
+ handle = openSync(lockPath, "wx", 0o600);
202
+ } catch {
203
+ throw new Error("codex_selection_concurrent_operation");
204
+ }
205
+ try {
206
+ return fn();
207
+ } finally {
208
+ closeSync(handle);
209
+ rmSync(lockPath, { force: true });
210
+ }
211
+ }
212
+
213
+ function atomicReplaceTree(source, target) {
214
+ const parent = dirname(target);
215
+ mkdirSync(parent, { recursive: true });
216
+ const staging = join(parent, `.${basename(target)}.phase98-next-${process.pid}`);
217
+ const previous = join(parent, `.${basename(target)}.phase98-prev-${process.pid}`);
218
+ copyExact(source, staging);
219
+ if (existsSync(target)) renameSync(target, previous);
220
+ try {
221
+ renameSync(staging, target);
222
+ rmSync(previous, { recursive: true, force: true });
223
+ } catch (error) {
224
+ rmSync(target, { recursive: true, force: true });
225
+ if (existsSync(previous)) renameSync(previous, target);
226
+ rmSync(staging, { recursive: true, force: true });
227
+ throw error;
228
+ }
229
+ }
230
+
231
+ export function activateCodexCandidateSelection(input) {
232
+ const {
233
+ paths,
234
+ snapshotManifestPath,
235
+ expectedSnapshotHash,
236
+ candidateVersion,
237
+ expectedCandidateHash,
238
+ approvalId,
239
+ approvedAt,
240
+ restoreBy,
241
+ now,
242
+ } = input;
243
+ if (!approvalId) throw new Error("codex_selection_approval_required");
244
+ const approvedMs = Date.parse(approvedAt);
245
+ const restoreMs = Date.parse(restoreBy);
246
+ const nowMs = Date.parse(now);
247
+ if (![approvedMs, restoreMs, nowMs].every(Number.isFinite)) {
248
+ throw new Error("codex_selection_window_invalid");
249
+ }
250
+ if (nowMs < approvedMs || nowMs >= restoreMs || restoreMs - approvedMs > MAX_CODEX_CANARY_WINDOW_MS) {
251
+ throw new Error("codex_selection_window_exceeds_limit");
252
+ }
253
+ return withSelectionLock(paths, () => {
254
+ const snapshot = readCanonicalManifest(snapshotManifestPath, expectedSnapshotHash);
255
+ if (stateHash(paths, snapshot.pluginVersion) !== snapshot.stateHash) {
256
+ throw new Error("codex_selection_current_state_drift");
257
+ }
258
+ const candidatePath = join(paths.cacheRoot, candidateVersion);
259
+ if (!existsSync(candidatePath) || treeHash(candidatePath) !== expectedCandidateHash) {
260
+ throw new Error("codex_selection_candidate_cache_mismatch");
261
+ }
262
+ atomicReplaceTree(candidatePath, paths.pluginRoot);
263
+ if (pluginVersion(paths.pluginRoot) !== candidateVersion || treeHash(paths.pluginRoot) !== expectedCandidateHash) {
264
+ throw new Error("codex_selection_candidate_activation_mismatch");
265
+ }
266
+ const activation = {
267
+ schemaVersion: CODEX_PLUGIN_SELECTION_SCHEMA_VERSION,
268
+ approvalId,
269
+ approvedAt,
270
+ activatedAt: now,
271
+ restoreBy,
272
+ snapshotHash: expectedSnapshotHash,
273
+ stableVersion: snapshot.pluginVersion,
274
+ candidateVersion,
275
+ candidateHash: expectedCandidateHash,
276
+ changedPaths: [paths.pluginRoot],
277
+ };
278
+ const activationPath = join(dirname(snapshotManifestPath), "activation.json");
279
+ writeFileSync(activationPath, `${JSON.stringify(activation, null, 2)}\n`, { mode: 0o600 });
280
+ return { ...activation, activationPath };
281
+ });
282
+ }
283
+
284
+ export function restoreCodexPluginSelection(input) {
285
+ const {
286
+ paths,
287
+ snapshotManifestPath,
288
+ expectedSnapshotHash,
289
+ activationPath,
290
+ now,
291
+ taskEvidence,
292
+ recoveryReason,
293
+ } = input;
294
+ return withSelectionLock(paths, () => {
295
+ const snapshot = readCanonicalManifest(snapshotManifestPath, expectedSnapshotHash);
296
+ const activation = JSON.parse(readFileSync(activationPath, "utf8"));
297
+ if (
298
+ activation.snapshotHash !== expectedSnapshotHash ||
299
+ activation.candidateHash !== treeHash(paths.pluginRoot)
300
+ ) throw new Error("codex_selection_active_candidate_drift");
301
+ if (!recoveryReason) {
302
+ if (!taskEvidence?.taskId || taskEvidence.resolvedCandidateHash !== activation.candidateHash) {
303
+ throw new Error("codex_selection_task_identity_mismatch");
304
+ }
305
+ }
306
+ for (const item of snapshot.items) copyExact(item.backupPath, item.sourcePath);
307
+ if (stateHash(paths, snapshot.pluginVersion) !== snapshot.stateHash) {
308
+ throw new Error("codex_selection_restore_hash_mismatch");
309
+ }
310
+ const deadlineExceeded = Date.parse(now) > Date.parse(activation.restoreBy);
311
+ const restoration = {
312
+ restored: true,
313
+ restoredAt: now,
314
+ deadlineExceeded,
315
+ recoveryReason: recoveryReason || null,
316
+ snapshotHash: expectedSnapshotHash,
317
+ stableVersion: snapshot.pluginVersion,
318
+ candidateVersion: activation.candidateVersion,
319
+ };
320
+ writeFileSync(
321
+ join(dirname(snapshotManifestPath), "restoration.json"),
322
+ `${JSON.stringify(restoration, null, 2)}\n`,
323
+ { mode: 0o600 }
324
+ );
325
+ return restoration;
326
+ });
327
+ }
328
+
329
+ export function readCodexPluginSelection({ paths, stableVersion } = {}) {
330
+ if (!paths) throw new Error("codex_selection_paths_required");
331
+ const version = pluginVersion(paths.pluginRoot);
332
+ const stable = stableVersion || version;
333
+ return {
334
+ pluginVersion: version,
335
+ pluginHash: treeHash(paths.pluginRoot),
336
+ configHash: treeHash(paths.configPath),
337
+ marketplaceHash: treeHash(paths.marketplacePath),
338
+ stableCacheHash: treeHash(join(paths.cacheRoot, stable)),
339
+ selectionHash: stateHash(paths, stable),
340
+ };
341
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.320",
3
+ "version": "0.1.322",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: refill-sends-v2
3
3
  description: Execute refill sends v2 through the fenced evergreen refill loop, with dry-run and resume support.
4
- visibility: public
4
+ visibility: internal
5
5
  allowed-tools:
6
- - mcp__sellable__refill_sends_v2
6
+ - mcp__sellable__refill_sends
7
7
  - mcp__sellable__get_refill_plan_v2
8
8
  - mcp__sellable__get_subskill_prompt
9
9
  - mcp__sellable__get_subskill_asset
@@ -24,7 +24,7 @@ Host command names:
24
24
  - Claude Code: `/sellable:refill-sends-v2`
25
25
  - Codex: `$sellable:refill-sends-v2`
26
26
 
27
- `refill_sends_v2` is the execution surface. In real-run mode it starts or
27
+ `refill_sends` is the execution surface. In real-run mode it starts or
28
28
  resumes a fenced refill run, reads a fresh packet, executes only the packet's
29
29
  named bounded work, verifies the result, and returns either a terminal report, a
30
30
  blocked report, or an in-progress resume handle. In dry-run mode it stays
@@ -40,19 +40,19 @@ with `WORKSPACE_REQUIRED`; do not switch the shared active workspace.
40
40
  For a real refill run:
41
41
 
42
42
  ```text
43
- refill_sends_v2({ workspaceId, intent:"auto", senderIds?, approvalMode? })
43
+ refill_sends({ workspaceId, intent:"auto", senderIds?, approvalMode? })
44
44
  ```
45
45
 
46
46
  For read-only inspection:
47
47
 
48
48
  ```text
49
- refill_sends_v2({ workspaceId, dryRun:true, intent:"auto", senderIds? })
49
+ refill_sends({ workspaceId, dryRun:true, intent:"auto", senderIds? })
50
50
  ```
51
51
 
52
52
  To resume an in-progress run, pass the handle back exactly:
53
53
 
54
54
  ```text
55
- refill_sends_v2({ workspaceId, runId, fence })
55
+ refill_sends({ workspaceId, runId, fence })
56
56
  ```
57
57
 
58
58
  If a stale handle loses the lease, the tool reports the holder status and the
@@ -58,394 +58,97 @@ allowed-tools:
58
58
 
59
59
  # Refill Sends
60
60
 
61
- Use this public command wrapper for plain operator requests such as "fill",
62
- "refill sends", "max out sends", "load everyone up", or "fill horizon sends".
63
-
64
- Host command names:
65
-
66
- - Claude Code: `/sellable:refill-sends`
67
- - Codex: `$sellable:refill-sends`
68
-
69
- Accepted invocation flags in the same user request:
70
-
71
- - `--yolo`: auto-accept the rendered bounded refill packet after the required
72
- fresh state reread.
73
- - `workspaceId: <id>`: required for scheduled automation and `--yolo`; pass it
74
- through on every refill MCP tool call instead of relying on shared config
75
- state.
76
- - `--sender <name-or-id>`: scope to a specific sender; repeat for multiple
77
- senders.
78
- - `--until <YYYY-MM-DD>`: fill through that sender-local date, inclusive,
79
- instead of the default scheduler-forward 48-hour target window.
80
- - `--target-date <YYYY-MM-DD>`: fill only that sender-local date. This means
81
- prepare rows for scheduler-fillable slots on that date, not every remaining
82
- daily-limit slot and not an inclusive through-date.
83
- - `senderIds: <id>, <id>` or `senderNames: <name>, <name>`: explicit selector
84
- alternatives when the host preserves natural-language arguments better than
85
- shell-style flags.
86
- - `actionTypes: send_invite, send_inmail_closed`: optional lane selector. Omit
87
- `actionTypes` to preserve the target planner's lane inference.
88
- - `untilDate: YYYY-MM-DD`: explicit date selector alternative when the host
89
- preserves natural-language arguments better than shell-style flags.
90
- - `targetDate: YYYY-MM-DD`: exact-date selector alternative when the host
91
- preserves natural-language arguments better than shell-style flags.
92
-
93
- When the host can call typed MCP tools, start with:
61
+ This is the concise public adapter for the typed refill contract. The backend
62
+ planner, durable run, flow asset, and contract asset own allocation, retries,
63
+ circuit behavior, scheduler placement, and completion rules.
64
+
65
+ Compatibility triggers include `fill`, `refill sends`, `max out sends`,
66
+ `load everyone up`, and `fill horizon sends`. Route every one through this same
67
+ typed workflow; none authorizes a broader or legacy execution path.
68
+
69
+ Measure completion from canonical projected coverage (`sent + scheduled`). The
70
+ workflow does not lower paid-InMail thresholds; a threshold change remains a
71
+ separate operator gate.
72
+
73
+ Exact zero-shot form:
74
+
75
+ `$sellable:refill-sends in sellable.dev workspace --target-date YYYY-MM-DD --yolo`
76
+
77
+ Claude Code uses `/sellable:refill-sends`. Optional selectors are `--sender`,
78
+ `--target-date`, `--until`, and `--yolo`.
79
+
80
+ ## Bootstrap and execute
81
+
82
+ 1. Call `mcp__sellable__get_auth_status({})`. If authentication is incomplete,
83
+ use the returned login path and stop until verified.
84
+ 2. First resolve and echo the explicit workspace and date scope. In scheduled or
85
+ `--yolo` mode, a missing `workspaceId` is `WORKSPACE_REQUIRED`; never change
86
+ shared active-workspace state to guess. Echo `workspaceId`, workspace name,
87
+ `targetDate` or other date selector, sender selectors, action selectors, and
88
+ approval mode before continuing.
89
+ 3. Load
90
+ `mcp__sellable__get_subskill_prompt({ subskillName: "refill-sends-workflow" })`
91
+ until `hasMore:false`.
92
+ 4. Load
93
+ `mcp__sellable__get_subskill_asset({ subskillName: "refill-sends-workflow", assetPath: "core/flow.v1.json" })`
94
+ until `hasMore:false`.
95
+ 5. Load
96
+ `mcp__sellable__get_subskill_asset({ subskillName: "refill-sends-workflow", assetPath: "core/contract.v2.json" })`
97
+ until `hasMore:false`. Validate `contractVersion`, `cacheVersion`, and
98
+ `contractHash` against the flow and generated metadata. Stop with
99
+ `unsupported_refill_contract_major` or `refill_contract_hash_mismatch` on
100
+ incompatibility; do not emulate missing assets from repo files or memory.
101
+ 6. Invoke only the public durable entrypoint:
102
+ `mcp__sellable__refill_sends({ workspaceId, targetDate, untilDate, horizonSendDays, senderIds, senderNames, actionTypes, yolo, executionMode, requireWorkspace:true })`.
103
+ Preserve the exact scope and approval echo returned by the tool on resume.
104
+
105
+ ## Operator packet
106
+
107
+ Always render the normal bounded approval packet, even under `--yolo`. Yolo is
108
+ bounded auto-accept of that packet; it is not broader authority. Use campaign
109
+ and sender names first, with IDs secondary. Preserve these six labels exactly:
110
+
111
+ - Need to prepare
112
+ - Goal
113
+ - Already sent
114
+ - Scheduled
115
+ - Ready and waiting to be scheduled
116
+ - Still need
117
+
118
+ Show `targetShapeRevision`, `stateRevision`, action key, exact target date,
119
+ side-effect class, expected targets, placement cap, and approval expiry. The
120
+ public tool runs one packet-named primitive, records its receipt, rereads
121
+ authoritative state, and replans. Keep any existing Codex goal open through
122
+ settling; a skill cannot create `/goal` by itself.
123
+
124
+ ## Safety and terminal truth
125
+
126
+ The generated contract is authoritative. Never direct-send, raw-write scheduler
127
+ fields, create/archive/delete campaigns, lower thresholds, switch an unselected
128
+ source family, mutate sender configuration, or start an unapproved campaign.
129
+ Paused campaign start and workspace-wide scheduler placement remain separately
130
+ named, product-gated side effects. Sellable MCP receipts are the proof surface;
131
+ never use Prisma, SQL, direct database access, or production scripts as proof.
132
+
133
+ Render typed terminal truth name-first. Success is `projected_full`. Otherwise
134
+ report the concrete blocker or `deadline_reached`, `iteration_limit`, or
135
+ `operator_stopped` with target-specific evidence. A ready buffer or
136
+ `awaiting_scheduler_after_ready_buffer` is
137
+ `ready_buffer_exists_is_not_complete`, never completion.
138
+
139
+ <!-- REFILL_CONTRACT_GENERATED:START -->
140
+ ## Generated refill contract metadata
141
+
142
+ contractVersion: 2.0.0
143
+ cacheVersion: refill-contract-v1
144
+ contractHash: e54ba0f80bc534fd70fd8c10c1caab109fe9a91c205cfc423f5f6735cd53fdc1
94
145
 
95
146
  ```text
96
- refill_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" })
147
+ scope: workspace_id, target_date, sender_selectors, campaign_selectors, approval_mode, contract_identity, installed_identity
148
+ gates: approval_packet, one_primitive, fresh_reread, normalized_accounting, prep_circuit
149
+ release identities (independent of semantic hash): mcpPackageVersion, installPackageVersion, codexPluginVersion
150
+ forbidden: direct_send, raw_scheduler_write, campaign_create, campaign_archive_delete, threshold_lowering, unselected_source_mutation, unapproved_campaign_start, sender_config_write
151
+ terminal: projected_full, concrete_blocker, deadline_reached, iteration_limit, operator_stopped, capped_by_scheduler, loaded_awaiting_scheduler, lanes_exhausted, not_an_evergreen_workspace, no_refillable_campaigns
152
+ ready_buffer_exists_is_not_complete
97
153
  ```
98
-
99
- That command helper normalizes arguments and returns the execution contract. In
100
- non-yolo mode it does not mutate. In `--yolo`, it may execute exactly one safe
101
- bounded primitive from the fresh `target.globalActionQueue[0]`, then reread and
102
- return the new target plan; currently safe primitives are paid-credit refresh,
103
- existing-row message preparation, generated-message approval, receipt-proven
104
- same-source row copy, a request-scoped exact-date product scheduler sweep, and
105
- read-only wait rereads. Same-source copy/source
106
- fallback is safe only after receipt-proven exhaustion:
107
- `hasMoreFrontierRows:false`, zero `approvalCandidates`, no `stuckActiveCells`,
108
- and no non-terminal `approvedNotDispatched` work. It does not run unbounded
109
- approval, lower paid-InMail thresholds, switch source families, create
110
- campaigns, launch, or send directly; it never raw-writes scheduler fields.
111
- Continue with the workflow below for route selection, state rereads, approval
112
- gating, source import, preparation, and bounded approval.
113
-
114
- The approval scope is the exact `workspaceId`, `targetDate`, sender ids, action
115
- types, campaign/table/source ids, `targetShapeRevision`, and `actionKey` in the
116
- rendered packet. `stateRevision` and sent/scheduled/ready counts are mutable
117
- progress, not approval-scope drift. Execute only
118
- `target.globalActionQueue[0]`, then perform a full authoritative reread before
119
- deciding any next action.
120
-
121
- ## Workspace Contract
122
-
123
- First resolve the target workspace id from the user's request, automation config,
124
- or install-time workspace mapping. Pass `workspaceId` on every scheduled or
125
- `--yolo` refill tool call, including setup/read calls such as
126
- `refill_sends`, `get_refill_target_plan`, `list_senders`,
127
- `get_sender_routing`, `resolve_campaign_fill_route`,
128
- `get_campaign_refill_state`, `get_scheduler_fill_capacity`,
129
- `run_scheduler_sweep`, and any later refill mutation covered by the packet.
130
- Missing `workspaceId` in scheduled or `--yolo` mode is a blocker; stop with
131
- `WORKSPACE_REQUIRED` instead of running against an implicit or guessed
132
- workspace.
133
-
134
- Do not solve scheduled or `--yolo` workspace uncertainty by changing the shared
135
- active workspace. Manual interactive workspace switching remains a separate
136
- diagnostic/setup flow, outside automation.
137
-
138
- Examples:
139
-
140
- ```text
141
- refill_sends({ yolo:true, executionMode:"yolo", requireWorkspace:true, workspaceId:"cmlq1v8ms0000jx04ang4hi7e" })
142
- get_refill_target_plan({ intent:"plain", approvalMode:"approve", workspaceId:"cmlq1v8ms0000jx04ang4hi7e" })
143
- list_senders({ workspaceId:"cmlq1v8ms0000jx04ang4hi7e" })
144
- ```
145
-
146
- Clover scheduled automation must name the Clover workspace explicitly:
147
-
148
- ```text
149
- refill_sends({ yolo:false, executionMode:"scheduled", requireWorkspace:true, workspaceId:"cmlq1v8ms0000jx04ang4hi7e" })
150
- get_refill_target_plan({ intent:"plain", approvalMode:"mark_ready", workspaceId:"cmlq1v8ms0000jx04ang4hi7e" })
151
- ```
152
-
153
- First call `get_auth_status({})`. If auth is not OK, follow the returned login
154
- guidance before route resolution. Do not run refill research against an implicit
155
- or guessed workspace.
156
-
157
- Treat "refill senders", "fill senders", "load everyone up", and "max out
158
- senders" as sender-scoped requests. The target set is senders enrolled in active
159
- campaign-backed sequence campaigns, not the first active campaign returned by a
160
- resolver.
161
-
162
- Goal-mode continuation: a skill cannot create or invoke `/goal` by itself. When
163
- this command is already running inside an active Codex goal, keep that goal open
164
- until every selected sender lane is filled by projected coverage
165
- (`sent + scheduled`) for the scheduler-forward target window, Christian
166
- explicitly stops/statuses the run, or a concrete non-scheduler blocker appears.
167
- Do not call the goal complete or blocked only because the current state is
168
- `awaiting_scheduler_after_ready_buffer`; treat it as loaded, awaiting scheduler.
169
-
170
- Load the internal workflow prompt and deterministic flow asset before taking
171
- any operational step:
172
-
173
- ```text
174
- get_subskill_prompt({ subskillName: "refill-sends-workflow" })
175
- get_subskill_asset({ subskillName: "refill-sends-workflow", assetPath: "core/flow.v1.json" })
176
- ```
177
-
178
- Continue both loads until `hasMore:false`; parse the flow JSON and verify
179
- `workflow:"refill-sends-workflow"` with a `v1` version. Then follow that
180
- workflow exactly. The default path is read-only research: compute the refill
181
- target plan first, resolve the route, identify the sender-relevant campaign
182
- that most recently had scheduler-owned sends, read refill state for that target,
183
- report the next safe step using campaign names first, and stop before mutation
184
- unless the user has explicitly approved the exact workspace,
185
- campaign/table/source ids, caps/dates, approval mode, expected side effects,
186
- and stop/rollback condition.
187
-
188
- Immediately after loading the internal workflow, call
189
- `get_refill_target_plan`. The target plan is the canonical first fact receipt:
190
- eligible senders, selected sender-local days, gross target, actual sent
191
- coverage, scheduler-owned scheduled coverage across active enrolled campaigns,
192
- projected coverage (`sent + scheduled`), inferred per-sender send lane/action
193
- selections, ready buffer, remaining projected gap, paid-InMail credit/threshold
194
- feasibility, `targetShapeRevision`, and `stateRevision`. Refill target lanes are
195
- connection invites (`send_invite`), standalone paid InMails
196
- (`send_inmail_closed`), or unified Sales Nav cascades represented publicly as
197
- `send_inmail_closed` with `campaignClassification:"sales_nav_cascade"`. For a
198
- Sales Nav cascade, refill the selected campaign first; its sequence can route
199
- prospects to Open InMail, paid InMail while fresh credits are >= 5, or
200
- same-campaign connection fallback without asking for separate open/paid/
201
- connection campaigns. DMs (`send_dm`) are follow-up actions, not refill horizon
202
- target capacity, and must not be counted as refill sent, scheduled, or ready
203
- coverage. When `actionTypes` are omitted, trust the target plan's inferred lane
204
- rather than asking which campaign class to fill. If a stale target plan selects
205
- `send_dm` or `send_inmail_open` as the lane, stop and re-plan with the current
206
- planner before mutation.
207
- Short form: trust the target plan's inferred lane when it is a connection invite,
208
- paid-InMail refill lane, or unified Sales Nav cascade.
209
- Connection-only evergreen campaigns with exact `["send_invite"]` sequence proof
210
- are plain invite capacity: treat them as `selectedLane:"send_invite"` only. Do
211
- not infer DM, InMail, Sales Nav cascade, paid-credit refresh, message
212
- generation, follow-up, sequence mutation, launch/start, scheduling override, or
213
- direct-send work from a connection-only lane.
214
-
215
- Structured planner packet:
216
-
217
- - `target.eligibleSenderLedger` is the public sender eligibility ledger.
218
- - `target.senderRefillPlans[]` is the canonical sender-level packet; read and
219
- display it before mutation.
220
- - Each sender packet includes `campaignRanking.options`, `sourcePlan`,
221
- `refillReceipt`, `nextActions`, and `manualAlternates`.
222
- - `refillReceipt` is the public ladder receipt. It carries the selected
223
- campaign/sender/lane summary, skipped rungs, existing-row frontier proof, and
224
- any absolute `wait.deadlineAt`.
225
- - Preserve these coverage labels exactly: `Need to prepare`, `Goal`,
226
- `Already sent`, `Scheduled`, `Ready and waiting to be scheduled`, and
227
- `Still need`.
228
- - `target.globalActionQueue` is the only cross-sender yolo execution queue.
229
- Execute exactly one globally ranked primitive from
230
- `target.globalActionQueue[0]`, then rerun `get_refill_target_plan` with the
231
- same `workspaceId` before
232
- choosing another action.
233
- - `manualAlternates` are not yolo actions. Threshold lowering and campaign
234
- creation are manual continuations only.
235
-
236
- Refill action ladder: approve generated rows only when an explicit bounded
237
- approval gate exists, process all existing same-campaign unenriched/unprepared
238
- rows in bounded batches before any source work, then copy bounded net-new rows
239
- from the selected source (`selectedLeadListId`, provider, and source
240
- fingerprint preserved), then use provider-aligned source-more. A new source or
241
- provider switch changes the reply-rate baseline and is a manual alternate, not a
242
- `--yolo` side effect.
243
- Source/copy/fallback requires receipt-proven exhaustion of earlier rungs:
244
- `existingRowFrontier.hasMoreFrontierRows:false`, zero `approvalCandidates`, no
245
- fresh active prep, no `stuckActiveCells`, and no non-terminal
246
- `approvedNotDispatched` rows. Treat anomalies, `stuckActiveCells`, and
247
- non-terminal `approvedNotDispatched` as diagnose-and-report gates, not
248
- exhaustion. Terminal `approvedNotDispatched` blockers may be reported, then the
249
- ladder can proceed.
250
-
251
- Run-local paid-credit guard: in `--yolo`, the `refill_sends` MCP command
252
- automatically maintains a `refreshedPaidInmailSenderIds` set for the current
253
- command call. If its first target plan has stale/missing paid-InMail credit
254
- facts, it refreshes each selected sender at most once, reruns
255
- `get_refill_target_plan`, and returns the post-refresh `targetPlan` before
256
- choosing the next prep/source-copy/bounded-approval/read-only wait action. If fresh facts are still below
257
- threshold, below-threshold paid-InMail facts fall back to an existing connection
258
- lane, the same Sales Nav cascade campaign's connection branch, or a manual
259
- continuation.
260
-
261
- Freshness gate precedes scheduler wait: if any selected
262
- `target.senderRefillPlans[].paidInmail.status` is `missing_credit_facts` or
263
- `stale_credit_facts`, or the target plan contains a
264
- `refresh_paid_inmail_credits` candidate, do not enter `wait_for_scheduler` even
265
- when `remainingReadyOrProjectedGap:0`. Refresh the exact selected sender credit
266
- facts once, rerun `get_refill_target_plan`, and only then decide whether
267
- scheduler wait is the next safe action. If facts remain missing/stale after the
268
- single refresh attempt, stop with a paid-InMail freshness blocker instead of
269
- waiting on scheduler pickup.
270
- The standalone MCP refresh surface is the scoped route
271
- `/api/v3/mcp/senders/:senderId/refresh-inmail-credits` with explicit
272
- `workspaceId`; do not use active-workspace mutation as the automation control
273
- path.
274
-
275
- Compact refill lessons: sender-level target plan is final truth; trust
276
- `schedulerGate.sendable` and scheduler gate blockers, not raw
277
- `unipileAccountStatus` labels alone; use compact prep status checks for
278
- long-running jobs; reread target plans after prep or cancel; treat ready rows as
279
- intermediate; avoid huge parallel target-plan reads when output is large. If a
280
- campaign produces prepared/ready rows but sender-level projected coverage does
281
- not move after one bounded settle loop, pivot to compact prep status or a
282
- scheduler-proven lane. Do not keep waiting on campaign-level ready counts.
283
-
284
- For exact-date requests, pass `targetDate` to `get_refill_target_plan`. If you
285
- need raw proof, call the read-only `get_scheduler_fill_capacity` query for the
286
- same sender/action/date; it tells the MCP how many cells the product scheduler
287
- will try to place and does not import, approve, schedule, refresh credits, or
288
- mutate.
289
- Treat `rollingWeeklyInvite.capacityFreedDuringWindow:true` as proof that
290
- rolling-weekly capacity frees later in the send window and remains schedulable;
291
- use its timing fields instead of treating the window-start gate as a permanent
292
- full-day blocker.
293
- When the refill loop has ready rows and needs scheduler pickup now, use
294
- `run_scheduler_sweep` with the same explicit `workspaceId`; it can place cells
295
- within existing scheduler gates and returns the receipt, but it never sends or
296
- bypasses limits.
297
- A scheduler sweep is a visible workspace-wide scheduling side effect. The
298
- request packet limits the expected target changes to its exact date, senders,
299
- action types, campaigns, tables, and stable sources, but the product scheduler
300
- may also place unrelated eligible workspace work; disclose that possibility in
301
- the approval packet and receipt. For an exact-date sweep action, execute it
302
- once, then reread the full target plan before taking or presenting another
303
- action.
304
- The exact-date `run_scheduler_sweep` may schedule eligible workspace cells
305
- through existing product gates; it never sends directly or raw-writes scheduler
306
- fields. Only non-exact or no-sweep scheduler waits are read-only.
307
- On resume, reconcile the current target plan and matching sweep status/receipt
308
- before retrying. A matching same-key terminal receipt replays; an
309
- `uncertain_outcome` stops for reconciliation and must not schedule again.
310
- Scheduler-run receipt interpretation: `cellsConsidered is allocation-attempt
311
- count`, not total ready supply, while `readyCellsFound` is ready inventory found
312
- before prefilters. Inspect `campaignScopeSummary` before assuming the selected
313
- refill campaign/table was included; if absent, do not infer that target was
314
- ready-but-blocked. Interpret `prefiltered` as ready cells removed before
315
- allocation, `skipped` as considered cells blocked by scheduler gates, and
316
- `deferred` as considered cells waiting on windows/capacity/cooldown. For ready
317
- closed-InMail cells with stale paid-credit prefilter/defer reasons, refresh
318
- paid-InMail credits once through existing tools, then rerun `run_scheduler_sweep`
319
- or read `action:"status"`; `refresh_paid_inmail_credits_then_rerun` is that
320
- path. `wait_for_capacity_or_window` means report loaded/capped/waiting and do
321
- not source or prep more rows; `no_ready_cells_continue_refill_prep` means return
322
- to the refill/prep ladder. Do not treat `cellsScheduled:0` alone as failure.
323
- If the target plan is complete by projected coverage, report that the selected
324
- target is already filled and no-op without asking for approval. If the ready
325
- buffer covers the projected gap, paid InMail credit facts are fresh for every
326
- selected paid-InMail lane, but scheduled coverage is still short, keep the run
327
- open in a persistent read-only scheduler wait loop. Poll
328
- `get_refill_target_plan` every 60-120 seconds, or on the host's next continuation
329
- interval, until projected coverage fills the target, a concrete non-scheduler
330
- blocker appears, or Christian explicitly asks to stop or only receive a status
331
- report. Treat `awaiting_scheduler_after_ready_buffer` as an in-progress wait
332
- state, not a close-out condition.
333
- Wait actions are gates, not competing goals. When `wait_for_active_work` or
334
- `wait_for_scheduler` includes receipt `wait.deadlineAt`, honor that absolute
335
- deadline; if it is expired on this call, escalate to diagnostics with the
336
- receipt evidence instead of issuing another blind wait.
337
- If paid InMail credit facts are stale or missing and the first target plan
338
- contains `refresh_paid_inmail_credits`, do not present that as the operator's
339
- next action in `--yolo`. Do not present paid-credit refresh as the next operator
340
- action after `refill_sends` returns `autoPaidInmailRefresh` and the
341
- post-refresh `targetPlan`. Trust `refill_sends.autoPaidInmailRefresh`: it should
342
- show the exact sender ids refreshed once, sender-credit-cache write receipts,
343
- and a returned post-refresh `targetPlan`. Continue from that post-refresh packet.
344
- If paid InMail is below threshold after the fresh credit read, report the exact
345
- campaign/table/column threshold action or same-campaign connection fallback;
346
- `--yolo` does not lower paid-InMail thresholds or create campaigns.
347
-
348
- If the plain route's managed waterfall targets are stale, for example skipped
349
- targets show archived/completed shared slots or the returned targets do not cover
350
- the named sender, immediately run the active route refetch before declaring a
351
- sender blocked. Current dashboard-active `PAUSED` campaign-backed sequence
352
- campaigns are start-eligible refill candidates; read refill state before deciding
353
- whether to prep, approve, start, or skip. Do not treat them as archived inventory.
354
-
355
- For interactive Codex or Claude Code sessions, that approval must use the
356
- host-native structured question gate, not plain chat:
357
-
358
- - Codex: use `request_user_input`.
359
- - Claude Code: use `AskUserQuestion`.
360
-
361
- The approval question must present the final refill step with exactly two
362
- choices: `Accept` and `Decline`. Render the full operator output packet in
363
- normal chat immediately before opening the structured approval question so it
364
- can be displayed as Markdown. The chat packet must include workspace, sender
365
- scope, a campaign-by-campaign table with campaign name, sender names, action,
366
- target count/cap, source/list, and blocker/skip reason, then exact ids,
367
- expected side effects, forbidden actions, and the stop condition. The structured
368
- question body must be compact and refer back to the posted packet instead of
369
- duplicating it. Treat `Accept` as permission for only the rendered target, caps,
370
- mode, and side effects. Treat `Decline` as a hard stop with no mutation.
371
-
372
- If Christian includes `--yolo` in the same refill request, treat that flag as
373
- auto-accept for the rendered bounded refill packet after the required fresh state
374
- reread. For sender-scoped language with no named senders, `--yolo` means all
375
- eligible healthy senders enrolled in active campaign-backed sequence campaigns in
376
- the requested `workspaceId`. Without `--yolo`, if Christian did not name senders, ask
377
- which eligible enrolled senders to refill before choosing campaigns or mutating.
378
-
379
- `--yolo` only covers the exact sender set, per-sender target campaigns, caps,
380
- approval mode, and side effects in the packet. It may authorize `start_campaign`
381
- only for selected `PAUSED`, dashboard-active, campaign-backed sequence refill
382
- targets when the rendered packet names the exact campaign ids and start side
383
- effect. Starting a paused campaign can let the product scheduler schedule/send
384
- approved eligible sequence actions, so include that expected side effect in the
385
- packet. It does not authorize starting unrelated, archived, completed, draft, or
386
- direct campaigns, separate launch/send actions, archive or delete cleanup,
387
- direct scheduler writes, sender reassignment, paid-InMail threshold changes,
388
- connection campaign creation, paid-InMail credit refreshes outside the exact
389
- rendered sender packet, or campaigns outside those
390
- selected for the eligible sender set. Stop and re-plan if the route, sender set,
391
- ids, caps, blockers, paid-InMail feasibility, action class, or side-effect class
392
- drift before mutation. Sent/scheduled counts increasing toward the approved
393
- target are expected progress:
394
- they change `stateRevision`, not `targetShapeRevision`, and do not require a
395
- second approval.
396
-
397
- In `--yolo`, continue as far as the rendered packet safely allows. After each
398
- apply/prep/source-copy/bounded-approval/read-only wait result, reread state, settle processing when needed, and move to
399
- the next selected sender or start-eligible same-packet campaign instead of
400
- stopping after the first partial result. If no in-packet safe action remains,
401
- return concrete continuation options with campaign names, exact ids, which option
402
- is still covered by the current packet, and which option needs a new approval
403
- packet.
404
-
405
- For `--yolo` fill/schedule requests, completion means projected saturation
406
- (`sent + scheduled`) for the scheduler-forward target window, not merely
407
- prepared/approved/ready rows. Maintain a target-window saturation ledger per
408
- selected sender: selected send days, gross capacity, actual sent cells, future
409
- scheduler-owned scheduled cells with non-null `scheduledFor`, projected count,
410
- ready-to-schedule buffer, remaining projected gap, paid-InMail feasibility,
411
- `targetShapeRevision`, `stateRevision`, and the next MCP primitive that can
412
- reduce the gap. After every apply/prep/source-copy/bounded-approval/read-only wait result, wait for processing, reread
413
- the target plan/refill state, recompute the ledger, then keep applying safe
414
- bounded actions until projected coverage fills the target window or a concrete
415
- non-scheduler blocker is proven.
416
- If ready rows cover the projected gap but the scheduler has not picked them up
417
- yet, report loaded, awaiting scheduler and run the persistent read-only
418
- scheduler wait loop. Do not finish the refill goal, mark it complete, or mark it
419
- blocked only because it is still `awaiting_scheduler_after_ready_buffer`; keep
420
- waiting unless Christian stops the run or the host cannot continue.
421
-
422
- Future scheduled coverage and already sent actions are distinct; only
423
- scheduler-owned future scheduled actions count as scheduled coverage. For
424
- multiple target dates, finish D1 execution and the full D1 reread before
425
- planning D2. Completion proof is product-native Sellable MCP evidence only:
426
- target plan, campaign refill state, scheduler capacity, sweep/status, and
427
- bounded receipts. Never use individual cell ids, Prisma, SQL, direct database
428
- access, or production-environment scripts as completion proof.
429
-
430
- In `--yolo`, the default fill target is the scheduler-forward 48-hour window
431
- unless `--target-date`/`targetDate`, `--until`/`untilDate`, or an explicit
432
- compatibility `horizonSendDays` is provided. If a target date is provided,
433
- compute bounded gaps for only that sender-local date using scheduler-fillable
434
- slots. If an until date is provided, compute bounded gaps through that
435
- sender-local date inclusive, skipping no-send days and never extending beyond
436
- that date without a new packet. For each target sender, compute the bounded gap
437
- from that sender's healthy daily capacity, existing future scheduler-owned
438
- scheduled sends, and rows already ready to schedule across active campaigns
439
- enrolled with that sender. Then pick the best same-sender campaign to fill the
440
- gap: prefer recent/future scheduler-owned sends for that sender, then strongest
441
- recent result evidence, then source health. Do not stop
442
- after filling only one sender when the request was sender-scoped. If a
443
- same-source copy hits the campaign-table row cap, split the current source into
444
- a bounded LinkedIn profile source list, confirm only that smaller list into the
445
- same campaign, and operate on the copied review batch. Do not fall back to
446
- on-demand campaigns or unrelated active-campaign fills.
447
-
448
- Public concepts are regular campaign and evergreen campaign. Internal direct
449
- campaign types are unsupported refill targets. `fill_campaign_horizon` is only a
450
- legacy evergreen-only lower-level primitive after route and refill-state
451
- evidence proves an evergreen target.
154
+ <!-- REFILL_CONTRACT_GENERATED:END -->