@sellable/install 0.1.218 → 0.1.219
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/sellable-install.mjs +26 -270
- package/package.json +1 -1
- package/skill-templates/create-campaign.md +14 -12
package/bin/sellable-install.mjs
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { dirname, join, relative } from "node:path";
|
|
15
15
|
import { stdout as output } from "node:process";
|
|
16
|
-
import { createInterface } from "node:readline/promises";
|
|
17
16
|
import { fileURLToPath } from "node:url";
|
|
18
17
|
import {
|
|
19
18
|
REQUIRED_SELLABLE_MCP_TOOLS,
|
|
@@ -23,7 +22,6 @@ import {
|
|
|
23
22
|
const DEFAULT_API_URL = "https://app.sellable.dev";
|
|
24
23
|
const DEFAULT_SERVER_PACKAGE =
|
|
25
24
|
process.env.SELLABLE_MCP_PACKAGE || "@sellable/mcp@latest";
|
|
26
|
-
const CODEX_INSTALL_URL = "https://chatgpt.com/codex/install";
|
|
27
25
|
|
|
28
26
|
function getInstallVersion() {
|
|
29
27
|
try {
|
|
@@ -177,8 +175,6 @@ Options:
|
|
|
177
175
|
--hosted-url <url> Hosted MCP URL for --server hosted.
|
|
178
176
|
--verbose Print every file write and shell command.
|
|
179
177
|
--dry-run Print actions without writing or running host commands.
|
|
180
|
-
--install-codex-cli Install Codex CLI without prompting if it is missing or blocked.
|
|
181
|
-
--no-install-codex-cli Do not prompt to install Codex CLI; install Desktop config only when possible.
|
|
182
178
|
--verify-only Verify installed host config where possible.
|
|
183
179
|
--json Print machine-readable verification JSON.
|
|
184
180
|
--artifact <path> Write verification JSON to a file.
|
|
@@ -251,7 +247,6 @@ function parseArgs(argv) {
|
|
|
251
247
|
json: false,
|
|
252
248
|
artifactPath: process.env.SELLABLE_VERIFY_ARTIFACT || "",
|
|
253
249
|
verbose: false,
|
|
254
|
-
codexCliInstall: codexCliInstallPreference(),
|
|
255
250
|
};
|
|
256
251
|
|
|
257
252
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -285,10 +280,6 @@ function parseArgs(argv) {
|
|
|
285
280
|
opts.hostedUrl = next();
|
|
286
281
|
} else if (arg === "--dry-run") {
|
|
287
282
|
opts.dryRun = true;
|
|
288
|
-
} else if (arg === "--install-codex-cli") {
|
|
289
|
-
opts.codexCliInstall = "install";
|
|
290
|
-
} else if (arg === "--no-install-codex-cli") {
|
|
291
|
-
opts.codexCliInstall = "skip";
|
|
292
283
|
} else if (arg === "--verify-only") {
|
|
293
284
|
opts.verifyOnly = true;
|
|
294
285
|
} else if (arg === "--json") {
|
|
@@ -312,14 +303,6 @@ function parseArgs(argv) {
|
|
|
312
303
|
return opts;
|
|
313
304
|
}
|
|
314
305
|
|
|
315
|
-
function codexCliInstallPreference() {
|
|
316
|
-
const raw = process.env.SELLABLE_INSTALL_CODEX_CLI || "";
|
|
317
|
-
const value = raw.trim().toLowerCase();
|
|
318
|
-
if (["1", "true", "yes", "y", "install"].includes(value)) return "install";
|
|
319
|
-
if (["0", "false", "no", "n", "skip"].includes(value)) return "skip";
|
|
320
|
-
return "ask";
|
|
321
|
-
}
|
|
322
|
-
|
|
323
306
|
function redact(value) {
|
|
324
307
|
if (!value) return "";
|
|
325
308
|
if (value.length <= 10) return "[redacted]";
|
|
@@ -377,175 +360,6 @@ function commandExistsDetails(command, platform = process.platform) {
|
|
|
377
360
|
};
|
|
378
361
|
}
|
|
379
362
|
|
|
380
|
-
function summarizeSpawnFailure(result) {
|
|
381
|
-
const errorMessage = result.error?.message || "";
|
|
382
|
-
const stderr = (result.stderr || "").trim();
|
|
383
|
-
const stdout = (result.stdout || "").trim();
|
|
384
|
-
return (
|
|
385
|
-
errorMessage ||
|
|
386
|
-
stderr ||
|
|
387
|
-
stdout ||
|
|
388
|
-
(Number.isInteger(result.status) ? `exit ${result.status}` : "unknown failure")
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function codexCliStatus(opts = {}) {
|
|
393
|
-
const details = commandExistsDetails("codex");
|
|
394
|
-
if (!details.exists) {
|
|
395
|
-
return {
|
|
396
|
-
exists: false,
|
|
397
|
-
usable: false,
|
|
398
|
-
reason: "Codex CLI not found on PATH",
|
|
399
|
-
details,
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
if (opts.dryRun) {
|
|
403
|
-
return {
|
|
404
|
-
exists: true,
|
|
405
|
-
usable: true,
|
|
406
|
-
reason: "dry-run",
|
|
407
|
-
details,
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const result = spawnSync("codex", ["mcp", "--help"], {
|
|
412
|
-
encoding: "utf8",
|
|
413
|
-
stdio: "pipe",
|
|
414
|
-
timeout: 10000,
|
|
415
|
-
});
|
|
416
|
-
if (result.status === 0) {
|
|
417
|
-
return {
|
|
418
|
-
exists: true,
|
|
419
|
-
usable: true,
|
|
420
|
-
reason: "Codex CLI responded to `codex mcp --help`",
|
|
421
|
-
details,
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
return {
|
|
426
|
-
exists: true,
|
|
427
|
-
usable: false,
|
|
428
|
-
reason: `Codex CLI found but not usable: ${summarizeSpawnFailure(result)}`,
|
|
429
|
-
details,
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
function codexDesktopLikelyInstalled(status = null) {
|
|
434
|
-
if (process.env.SELLABLE_INSTALL_ASSUME_CODEX_DESKTOP === "1") return true;
|
|
435
|
-
const home = codexHome();
|
|
436
|
-
const probes = [
|
|
437
|
-
status?.details?.stdout || "",
|
|
438
|
-
process.env.PATH || "",
|
|
439
|
-
].join("\n");
|
|
440
|
-
|
|
441
|
-
if (/WindowsApps[\\/]+codex\.exe/i.test(probes)) return true;
|
|
442
|
-
if (existsSync(join(home, "config.toml"))) return true;
|
|
443
|
-
if (existsSync(join(home, "plugins", ".plugin-appserver"))) return true;
|
|
444
|
-
if (existsSync(join(home, "plugins", "cache"))) return true;
|
|
445
|
-
if (process.platform === "darwin" && existsSync("/Applications/Codex.app")) {
|
|
446
|
-
return true;
|
|
447
|
-
}
|
|
448
|
-
return false;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
function isNonInteractiveInstall() {
|
|
452
|
-
return (
|
|
453
|
-
process.env.CI === "true" ||
|
|
454
|
-
process.env.SELLABLE_NON_INTERACTIVE === "1" ||
|
|
455
|
-
process.env.SELLABLE_NON_INTERACTIVE === "true" ||
|
|
456
|
-
process.env.SELLABLE_NON_INTERACTIVE === "yes"
|
|
457
|
-
);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
async function askYesNo(question, defaultAnswer = false) {
|
|
461
|
-
if (!process.stdin.isTTY || !output.isTTY || isNonInteractiveInstall()) {
|
|
462
|
-
return null;
|
|
463
|
-
}
|
|
464
|
-
const suffix = defaultAnswer ? " [Y/n] " : " [y/N] ";
|
|
465
|
-
const rl = createInterface({
|
|
466
|
-
input: process.stdin,
|
|
467
|
-
output: process.stdout,
|
|
468
|
-
});
|
|
469
|
-
try {
|
|
470
|
-
const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
|
|
471
|
-
if (!answer) return defaultAnswer;
|
|
472
|
-
return ["y", "yes"].includes(answer);
|
|
473
|
-
} finally {
|
|
474
|
-
rl.close();
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
function codexInstallCommand(platform = process.platform) {
|
|
479
|
-
const override = process.env.SELLABLE_CODEX_INSTALL_COMMAND;
|
|
480
|
-
if (override) {
|
|
481
|
-
return isWindowsPlatform(platform)
|
|
482
|
-
? ["cmd.exe", ["/d", "/s", "/c", override]]
|
|
483
|
-
: ["sh", ["-c", override]];
|
|
484
|
-
}
|
|
485
|
-
if (isWindowsPlatform(platform)) {
|
|
486
|
-
return [
|
|
487
|
-
"powershell.exe",
|
|
488
|
-
[
|
|
489
|
-
"-NoProfile",
|
|
490
|
-
"-ExecutionPolicy",
|
|
491
|
-
"Bypass",
|
|
492
|
-
"-Command",
|
|
493
|
-
`$env:CODEX_NON_INTERACTIVE='1'; irm ${CODEX_INSTALL_URL}.ps1 | iex`,
|
|
494
|
-
],
|
|
495
|
-
];
|
|
496
|
-
}
|
|
497
|
-
return [
|
|
498
|
-
"sh",
|
|
499
|
-
["-c", `curl -fsSL ${CODEX_INSTALL_URL}.sh | CODEX_NON_INTERACTIVE=1 sh`],
|
|
500
|
-
];
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
function codexManualInstallCommand(platform = process.platform) {
|
|
504
|
-
return isWindowsPlatform(platform)
|
|
505
|
-
? `$env:CODEX_NON_INTERACTIVE=1; irm ${CODEX_INSTALL_URL}.ps1 | iex`
|
|
506
|
-
: `curl -fsSL ${CODEX_INSTALL_URL}.sh | sh`;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
async function maybeInstallCodexCli(status, opts) {
|
|
510
|
-
if (opts.dryRun) return false;
|
|
511
|
-
if (opts.codexCliInstall === "skip") {
|
|
512
|
-
logWarn(`${status.reason}. Skipping Codex CLI install by request.`);
|
|
513
|
-
return false;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
let shouldInstall = opts.codexCliInstall === "install";
|
|
517
|
-
if (!shouldInstall) {
|
|
518
|
-
const answer = await askYesNo(
|
|
519
|
-
`${status.reason}. Install the Codex CLI now?`,
|
|
520
|
-
false
|
|
521
|
-
);
|
|
522
|
-
if (answer === null) {
|
|
523
|
-
logWarn(
|
|
524
|
-
`${status.reason}. Non-interactive shell; skipping Codex CLI install. Re-run with --install-codex-cli to install it.`
|
|
525
|
-
);
|
|
526
|
-
return false;
|
|
527
|
-
}
|
|
528
|
-
shouldInstall = answer;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
if (!shouldInstall) {
|
|
532
|
-
logWarn(`${status.reason}. Continuing with Codex Desktop plugin setup only.`);
|
|
533
|
-
return false;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
const [command, args] = codexInstallCommand();
|
|
537
|
-
logStep("Installing Codex CLI using the official Codex installer...");
|
|
538
|
-
try {
|
|
539
|
-
run(command, args, opts);
|
|
540
|
-
return true;
|
|
541
|
-
} catch (err) {
|
|
542
|
-
logWarn(
|
|
543
|
-
`Codex CLI install failed: ${err instanceof Error ? err.message : String(err)}`
|
|
544
|
-
);
|
|
545
|
-
return false;
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
363
|
function shellQuote(value) {
|
|
550
364
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
551
365
|
}
|
|
@@ -925,9 +739,9 @@ const CREATE_CAMPAIGN_ALLOWED_TOOLS = [
|
|
|
925
739
|
"mcp__sellable__save_rubrics",
|
|
926
740
|
"mcp__sellable__get_campaign_table_schema",
|
|
927
741
|
"mcp__sellable__select_campaign_cells",
|
|
742
|
+
"mcp__sellable__record_campaign_review_batch",
|
|
928
743
|
"mcp__sellable__queue_campaign_cells",
|
|
929
744
|
"mcp__sellable__wait_for_campaign_processing",
|
|
930
|
-
"mcp__sellable__fill_campaign_horizon",
|
|
931
745
|
"mcp__sellable__start_campaign_message_preparation",
|
|
932
746
|
"mcp__sellable__get_campaign_message_preparation_status",
|
|
933
747
|
"mcp__sellable__cancel_campaign_message_preparation",
|
|
@@ -2641,27 +2455,6 @@ function writeAuth(opts) {
|
|
|
2641
2455
|
return { written: true, reused: false };
|
|
2642
2456
|
}
|
|
2643
2457
|
|
|
2644
|
-
function resolveWindowsNpmCommand() {
|
|
2645
|
-
const explicit =
|
|
2646
|
-
process.env.SELLABLE_INSTALL_NPM_CMD_COMMAND ||
|
|
2647
|
-
process.env.SELLABLE_INSTALL_NPM_COMMAND;
|
|
2648
|
-
if (explicit) return explicit;
|
|
2649
|
-
|
|
2650
|
-
const nodeBin = process.env.SELLABLE_INSTALL_NODE_BIN || "";
|
|
2651
|
-
for (const name of ["npm.cmd", "npm"]) {
|
|
2652
|
-
const candidate = nodeBin ? join(nodeBin, name) : "";
|
|
2653
|
-
if (candidate && existsSync(candidate)) return candidate;
|
|
2654
|
-
}
|
|
2655
|
-
|
|
2656
|
-
if (isWindowsPlatform()) {
|
|
2657
|
-
const details = commandExistsDetails("npm.cmd");
|
|
2658
|
-
const first = details.stdout.split(/\r?\n/).find((line) => line.trim());
|
|
2659
|
-
if (details.exists && first) return first.trim();
|
|
2660
|
-
}
|
|
2661
|
-
|
|
2662
|
-
return packageManagerCommand("win32");
|
|
2663
|
-
}
|
|
2664
|
-
|
|
2665
2458
|
function installSelfShim(opts) {
|
|
2666
2459
|
const installDir =
|
|
2667
2460
|
process.env.SELLABLE_INSTALL_DIR || join(homedir(), ".local", "bin");
|
|
@@ -2682,11 +2475,12 @@ exec ${shellQuote(npmCommand)} exec --yes --package ${shellQuote(INSTALL_PACKAGE
|
|
|
2682
2475
|
isWindowsPlatform() || process.env.SELLABLE_INSTALL_WRITE_CMD_SHIM === "1";
|
|
2683
2476
|
if (shouldWriteCmdShim) {
|
|
2684
2477
|
const cmdPath = `${binPath}.cmd`;
|
|
2685
|
-
const npmCmdCommand =
|
|
2478
|
+
const npmCmdCommand =
|
|
2479
|
+
process.env.SELLABLE_INSTALL_NPM_CMD_COMMAND ||
|
|
2480
|
+
process.env.SELLABLE_INSTALL_NPM_COMMAND ||
|
|
2481
|
+
packageManagerCommand("win32");
|
|
2686
2482
|
const cmdPathPrefix = nodeBin ? `set "PATH=${nodeBin};%PATH%"\r\n` : "";
|
|
2687
|
-
const cmdShim =
|
|
2688
|
-
`@echo off\r\n${cmdPathPrefix}` +
|
|
2689
|
-
`call "${npmCmdCommand}" exec --yes --package ${INSTALL_PACKAGE_SPEC} -- sellable %*\r\n`;
|
|
2483
|
+
const cmdShim = `@echo off\r\n${cmdPathPrefix}"${npmCmdCommand}" exec --yes --package ${INSTALL_PACKAGE_SPEC} -- sellable %*\r\n`;
|
|
2690
2484
|
writeFile(cmdPath, cmdShim, opts, 0o755);
|
|
2691
2485
|
}
|
|
2692
2486
|
}
|
|
@@ -3026,70 +2820,32 @@ function patchClaudeAlwaysLoad(opts) {
|
|
|
3026
2820
|
}
|
|
3027
2821
|
}
|
|
3028
2822
|
|
|
3029
|
-
function
|
|
3030
|
-
|
|
3031
|
-
if (opts.server !== "hosted") {
|
|
3032
|
-
run("codex", ["mcp", "remove", "sellable"], {
|
|
3033
|
-
...opts,
|
|
3034
|
-
dryRun: opts.dryRun,
|
|
3035
|
-
allowFail: true,
|
|
3036
|
-
});
|
|
3037
|
-
}
|
|
3038
|
-
run("codex", codexMcpAddArgs(opts), opts);
|
|
3039
|
-
return true;
|
|
3040
|
-
} catch (err) {
|
|
3041
|
-
logWarn(
|
|
3042
|
-
`Codex CLI MCP registration skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
3043
|
-
);
|
|
3044
|
-
return false;
|
|
3045
|
-
}
|
|
3046
|
-
}
|
|
3047
|
-
|
|
3048
|
-
async function installCodex(opts) {
|
|
3049
|
-
let cliStatus = codexCliStatus(opts);
|
|
3050
|
-
let desktopLikely = codexDesktopLikelyInstalled(cliStatus);
|
|
3051
|
-
let attemptedCliInstall = false;
|
|
3052
|
-
|
|
3053
|
-
if (!opts.dryRun && !cliStatus.usable) {
|
|
3054
|
-
attemptedCliInstall = await maybeInstallCodexCli(cliStatus, opts);
|
|
3055
|
-
if (attemptedCliInstall) {
|
|
3056
|
-
cliStatus = codexCliStatus(opts);
|
|
3057
|
-
desktopLikely = codexDesktopLikelyInstalled(cliStatus) || desktopLikely;
|
|
3058
|
-
if (!cliStatus.usable) {
|
|
3059
|
-
logWarn(
|
|
3060
|
-
`${cliStatus.reason}. Continuing with Codex Desktop plugin setup where possible.`
|
|
3061
|
-
);
|
|
3062
|
-
}
|
|
3063
|
-
}
|
|
3064
|
-
}
|
|
3065
|
-
|
|
3066
|
-
const shouldInstallDesktop =
|
|
3067
|
-
opts.host === "codex" ||
|
|
3068
|
-
cliStatus.usable ||
|
|
3069
|
-
desktopLikely ||
|
|
3070
|
-
attemptedCliInstall;
|
|
3071
|
-
if (!shouldInstallDesktop) {
|
|
2823
|
+
function installCodex(opts) {
|
|
2824
|
+
if (!opts.dryRun && !commandExists("codex")) {
|
|
3072
2825
|
const message =
|
|
3073
|
-
"Codex CLI not found
|
|
2826
|
+
"Codex CLI not found. Install/login to Codex, then rerun: sellable --host codex";
|
|
3074
2827
|
if (opts.host === "all") {
|
|
3075
2828
|
logWarn(`Skipping Codex: ${message}`);
|
|
3076
2829
|
return { installed: false };
|
|
3077
2830
|
}
|
|
3078
2831
|
throw new Error(message);
|
|
3079
2832
|
}
|
|
3080
|
-
|
|
3081
2833
|
if (!opts.dryRun) {
|
|
3082
2834
|
mkdirSync(codexHome(), { recursive: true, mode: 0o700 });
|
|
3083
2835
|
}
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
"Codex CLI MCP registration did not run; writing Codex Desktop plugin and config directly."
|
|
3089
|
-
);
|
|
2836
|
+
if (opts.server === "hosted") {
|
|
2837
|
+
run("codex", codexMcpAddArgs(opts), opts);
|
|
2838
|
+
const info = installCodexDesktopPlugin(opts);
|
|
2839
|
+
return { installed: true, ...info };
|
|
3090
2840
|
}
|
|
2841
|
+
run("codex", ["mcp", "remove", "sellable"], {
|
|
2842
|
+
...opts,
|
|
2843
|
+
dryRun: opts.dryRun,
|
|
2844
|
+
allowFail: true,
|
|
2845
|
+
});
|
|
2846
|
+
run("codex", codexMcpAddArgs(opts), opts);
|
|
3091
2847
|
const info = installCodexDesktopPlugin(opts);
|
|
3092
|
-
return { installed: true,
|
|
2848
|
+
return { installed: true, ...info };
|
|
3093
2849
|
}
|
|
3094
2850
|
|
|
3095
2851
|
function requiredCheck(ok, label) {
|
|
@@ -3240,11 +2996,11 @@ async function verify(opts) {
|
|
|
3240
2996
|
);
|
|
3241
2997
|
}
|
|
3242
2998
|
if (opts.host === "codex" || opts.host === "all") {
|
|
3243
|
-
const
|
|
2999
|
+
const hasCodexCli = commandExists("codex");
|
|
3244
3000
|
checks.push(
|
|
3245
3001
|
warningCheck(
|
|
3246
|
-
|
|
3247
|
-
|
|
3002
|
+
hasCodexCli,
|
|
3003
|
+
hasCodexCli ? "Codex CLI present" : "Codex CLI missing"
|
|
3248
3004
|
)
|
|
3249
3005
|
);
|
|
3250
3006
|
const pluginPath = join(
|
|
@@ -3602,8 +3358,8 @@ function printNextSteps(installedHosts, authReused) {
|
|
|
3602
3358
|
console.log("");
|
|
3603
3359
|
printInstallAgentBox(
|
|
3604
3360
|
"Install Codex",
|
|
3605
|
-
|
|
3606
|
-
"https://
|
|
3361
|
+
"npm install -g @openai/codex",
|
|
3362
|
+
"https://github.com/openai/codex"
|
|
3607
3363
|
);
|
|
3608
3364
|
console.log("");
|
|
3609
3365
|
console.log("");
|
|
@@ -4049,7 +3805,7 @@ async function main() {
|
|
|
4049
3805
|
}
|
|
4050
3806
|
}
|
|
4051
3807
|
if (opts.host === "codex" || opts.host === "all") {
|
|
4052
|
-
const result =
|
|
3808
|
+
const result = installCodex(opts);
|
|
4053
3809
|
if (result.installed) {
|
|
4054
3810
|
installedHosts.push("Codex");
|
|
4055
3811
|
}
|
package/package.json
CHANGED
|
@@ -44,9 +44,9 @@ allowed-tools:
|
|
|
44
44
|
- mcp__sellable__save_rubrics
|
|
45
45
|
- mcp__sellable__get_campaign_table_schema
|
|
46
46
|
- mcp__sellable__select_campaign_cells
|
|
47
|
+
- mcp__sellable__record_campaign_review_batch
|
|
47
48
|
- mcp__sellable__queue_campaign_cells
|
|
48
49
|
- mcp__sellable__wait_for_campaign_processing
|
|
49
|
-
- mcp__sellable__fill_campaign_horizon
|
|
50
50
|
- mcp__sellable__start_campaign_message_preparation
|
|
51
51
|
- mcp__sellable__get_campaign_message_preparation_status
|
|
52
52
|
- mcp__sellable__cancel_campaign_message_preparation
|
|
@@ -105,16 +105,15 @@ most one direct `enrich_with_prospeo` sample when row evidence is too thin.
|
|
|
105
105
|
After filter approval, the browser should move to Filter Leads with
|
|
106
106
|
`currentStep: "apply-icp-rubric"` and show template waiting/approval copy until
|
|
107
107
|
the template is approved.
|
|
108
|
+
If the bounded filter run later returns `0/N` passes, do not immediately find
|
|
109
|
+
new leads. Load `references/sample-validation-loop.md`, run the zero-pass
|
|
110
|
+
rule-relaxability audit, then choose exactly one recovery: revise saved
|
|
111
|
+
rubrics, record a fresh same-source review batch, or change lead source. Change
|
|
112
|
+
source only when safe relaxation and same-source sampling would still fail or
|
|
113
|
+
would pass bad-fit rows.
|
|
108
114
|
The default path stays the existing first campaign-table execution slice:
|
|
109
115
|
review the normal `reviewBatchLimit:15`, approve reviewed draft rows, then move
|
|
110
116
|
to Settings/sequence/final greenlight. Only call
|
|
111
|
-
`fill_campaign_horizon` for explicit source-cleanup horizon-fill requests, such
|
|
112
|
-
as "fill sends from Signal Discovery but not John Cutler posts." Run
|
|
113
|
-
`fill_campaign_horizon({ action:"audit", ... })` first, then apply with the
|
|
114
|
-
returned `stateRevision`. It imports/prepares at most 300 eligible non-excluded
|
|
115
|
-
source rows in the first pass, caps prep batches at 100, skips existing rows
|
|
116
|
-
from excluded post/author sources, and does not launch the campaign. If the
|
|
117
|
-
user only asks for generic extra sends with no source cleanup, use
|
|
118
117
|
`start_campaign_message_preparation` when the user explicitly asks for more
|
|
119
118
|
prepared messages, a send count, or language like "fill up/load sends for these
|
|
120
119
|
senders." Treat those requests as capacity-fill preparation: calculate the
|
|
@@ -128,9 +127,8 @@ count `checkedRows` as enriched rows; it is only the table cursor. Use
|
|
|
128
127
|
messages", set `targetPreparedMessages:X`, omit `maxRowsToCheck`, and keep
|
|
129
128
|
`approvalMode:"mark_ready"`. The backend calibrates on at least 100 actually
|
|
130
129
|
enriched rows, estimates the row budget from observed rubric/pass yield, caps
|
|
131
|
-
`maxRowsToCheck` at
|
|
132
|
-
|
|
133
|
-
has queueable or active cells. If the user says "approve X messages", use
|
|
130
|
+
`maxRowsToCheck` at 2500, then adapts later batches up to 250 rows while
|
|
131
|
+
recalculating yield. If the user says "approve X messages", use
|
|
134
132
|
`approvalMode:"approve"` but still do not launch. If the user says "schedule X
|
|
135
133
|
sends" or asks to fill sender sends, use `approvalMode:"approve"` to approve
|
|
136
134
|
exactly the bounded X-message cohort during preparation, then continue through
|
|
@@ -1081,7 +1079,11 @@ updates.
|
|
|
1081
1079
|
rubrics and Message Drafting runs.
|
|
1082
1080
|
After rubrics save, keep Filter Rules visible for approval; after approval,
|
|
1083
1081
|
move to Filter Leads with `currentStep: "apply-icp-rubric"` and wait there
|
|
1084
|
-
while Message Drafting finishes or the template is approved.
|
|
1082
|
+
while Message Drafting finishes or the template is approved. After template
|
|
1083
|
+
approval and bounded scoring, a `0/N` pass result must run the
|
|
1084
|
+
sample-validation zero-pass rule-relaxability audit before any lead-source
|
|
1085
|
+
revision; the three allowed recoveries are rubric revision, fresh
|
|
1086
|
+
same-source sample, or source revision.
|
|
1085
1087
|
If filters are skipped, launch Message Drafting before moving to
|
|
1086
1088
|
Messages/message review; updating `currentStep` to `messages` is not proof
|
|
1087
1089
|
that the background worker started. Queue the bounded campaign-table
|