@sellable/install 0.1.327 → 0.1.329
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/HERMES-SLACK-PROFILE-SCOPING.md +169 -0
- package/README.md +41 -5
- package/agents/registry.json +2 -2
- package/bin/sellable-install.mjs +385 -112
- package/lib/codex-plugin-selection.mjs +442 -0
- package/lib/runtime-verify.mjs +2 -31
- package/package.json +3 -2
- package/skill-templates/create-campaign.md +3 -3
- package/skill-templates/create-evergreen-campaigns.md +16 -16
- package/skill-templates/refill-sends-v2.md +6 -6
- package/skill-templates/refill-sends.md +91 -353
- package/skill-templates/find-leads.md +0 -57
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
closeSync,
|
|
5
|
+
cpSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
openSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
readlinkSync,
|
|
12
|
+
readdirSync,
|
|
13
|
+
renameSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
symlinkSync,
|
|
16
|
+
writeFileSync,
|
|
17
|
+
} from "node:fs";
|
|
18
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
19
|
+
|
|
20
|
+
export const CODEX_PLUGIN_SELECTION_SCHEMA_VERSION = 1;
|
|
21
|
+
export const MAX_CODEX_CANARY_WINDOW_MS = 90 * 60 * 1000;
|
|
22
|
+
|
|
23
|
+
export function resolveCodexPluginSelectionPaths({
|
|
24
|
+
home = "",
|
|
25
|
+
marketplaceRoot = join(home, ".sellable", "codex-marketplace"),
|
|
26
|
+
cacheRoot = join(home, ".codex", "plugins", "cache", "sellable", "sellable"),
|
|
27
|
+
} = {}) {
|
|
28
|
+
if (!home) throw new Error("codex_selection_home_required");
|
|
29
|
+
return {
|
|
30
|
+
home: resolve(home),
|
|
31
|
+
configPath: join(home, ".codex", "config.toml"),
|
|
32
|
+
marketplaceRoot: resolve(marketplaceRoot),
|
|
33
|
+
marketplacePath: join(marketplaceRoot, ".agents", "plugins", "marketplace.json"),
|
|
34
|
+
pluginRoot: join(marketplaceRoot, "plugins", "sellable"),
|
|
35
|
+
cacheRoot: resolve(cacheRoot),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function canonicalize(value) {
|
|
40
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
41
|
+
if (value && typeof value === "object") {
|
|
42
|
+
return Object.fromEntries(
|
|
43
|
+
Object.keys(value)
|
|
44
|
+
.sort()
|
|
45
|
+
.map((key) => [key, canonicalize(value[key])])
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function digest(value) {
|
|
52
|
+
return createHash("sha256").update(value).digest("hex");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function canonicalDigest(value) {
|
|
56
|
+
return digest(JSON.stringify(canonicalize(value)));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function treeRecords(root) {
|
|
60
|
+
if (!existsSync(root)) return [{ path: ".", type: "missing" }];
|
|
61
|
+
const records = [];
|
|
62
|
+
const visit = (path) => {
|
|
63
|
+
const stat = lstatSync(path);
|
|
64
|
+
const rel = relative(root, path) || ".";
|
|
65
|
+
if (stat.isSymbolicLink()) {
|
|
66
|
+
records.push({ path: rel, type: "symlink", target: readlinkSync(path) });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (stat.isDirectory()) {
|
|
70
|
+
records.push({ path: rel, type: "directory", mode: stat.mode & 0o777 });
|
|
71
|
+
for (const name of readdirSync(path).sort()) visit(join(path, name));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
records.push({
|
|
75
|
+
path: rel,
|
|
76
|
+
type: "file",
|
|
77
|
+
mode: stat.mode & 0o777,
|
|
78
|
+
sha256: digest(readFileSync(path)),
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
visit(root);
|
|
82
|
+
return records;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function treeHash(root) {
|
|
86
|
+
return canonicalDigest(treeRecords(root));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function quoteToml(value) {
|
|
90
|
+
return JSON.stringify(String(value));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function removeTomlTable(content, header) {
|
|
94
|
+
const lines = content.split("\n");
|
|
95
|
+
const target = `[${header}]`;
|
|
96
|
+
const kept = [];
|
|
97
|
+
let skipping = false;
|
|
98
|
+
for (const line of lines) {
|
|
99
|
+
const trimmed = line.trim();
|
|
100
|
+
if (/^\[[^\]]+\]$/.test(trimmed)) {
|
|
101
|
+
skipping = trimmed === target;
|
|
102
|
+
if (skipping) continue;
|
|
103
|
+
}
|
|
104
|
+
if (!skipping) kept.push(line);
|
|
105
|
+
}
|
|
106
|
+
return kept.join("\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function candidateCodexConfig(content, candidatePath) {
|
|
110
|
+
const mcpPath = join(candidatePath, ".mcp.json");
|
|
111
|
+
if (!existsSync(mcpPath)) throw new Error("codex_candidate_mcp_manifest_missing");
|
|
112
|
+
const manifest = JSON.parse(readFileSync(mcpPath, "utf8"));
|
|
113
|
+
const server = manifest?.mcpServers?.sellable;
|
|
114
|
+
if (
|
|
115
|
+
server?.type !== "stdio" ||
|
|
116
|
+
typeof server.command !== "string" ||
|
|
117
|
+
!Array.isArray(server.args) ||
|
|
118
|
+
server.args.some((value) => typeof value !== "string") ||
|
|
119
|
+
!server.env ||
|
|
120
|
+
typeof server.env !== "object" ||
|
|
121
|
+
Object.values(server.env).some((value) => typeof value !== "string")
|
|
122
|
+
) {
|
|
123
|
+
throw new Error("codex_candidate_mcp_manifest_invalid");
|
|
124
|
+
}
|
|
125
|
+
let next = removeTomlTable(content, "mcp_servers.sellable.env");
|
|
126
|
+
next = removeTomlTable(next, "mcp_servers.sellable").trimEnd();
|
|
127
|
+
const args = `[${server.args.map(quoteToml).join(", ")}]`;
|
|
128
|
+
const env = Object.entries(server.env)
|
|
129
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
130
|
+
.map(([key, value]) => `${key} = ${quoteToml(value)}`)
|
|
131
|
+
.join("\n");
|
|
132
|
+
return `${next}\n\n[mcp_servers.sellable]\ncommand = ${quoteToml(
|
|
133
|
+
server.command
|
|
134
|
+
)}\nargs = ${args}\n\n[mcp_servers.sellable.env]\n${env}\n`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function pluginVersion(pluginRoot) {
|
|
138
|
+
const manifestPath = join(pluginRoot, ".codex-plugin", "plugin.json");
|
|
139
|
+
if (!existsSync(manifestPath)) throw new Error("codex_selection_plugin_manifest_missing");
|
|
140
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
141
|
+
if (manifest.name !== "sellable" || typeof manifest.version !== "string") {
|
|
142
|
+
throw new Error("codex_selection_plugin_manifest_invalid");
|
|
143
|
+
}
|
|
144
|
+
return manifest.version;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function ensureInside(parent, child, code) {
|
|
148
|
+
const root = resolve(parent);
|
|
149
|
+
const target = resolve(child);
|
|
150
|
+
if (!(target === root || target.startsWith(`${root}/`))) throw new Error(code);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function copyExact(source, target) {
|
|
154
|
+
rmSync(target, { recursive: true, force: true });
|
|
155
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
156
|
+
cpSync(source, target, {
|
|
157
|
+
recursive: true,
|
|
158
|
+
dereference: false,
|
|
159
|
+
preserveTimestamps: true,
|
|
160
|
+
});
|
|
161
|
+
syncExactModes(source, target);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function syncExactModes(source, target) {
|
|
165
|
+
const stat = lstatSync(source);
|
|
166
|
+
if (stat.isSymbolicLink()) return;
|
|
167
|
+
chmodSync(target, stat.mode & 0o777);
|
|
168
|
+
if (!stat.isDirectory()) return;
|
|
169
|
+
for (const name of readdirSync(source)) {
|
|
170
|
+
syncExactModes(join(source, name), join(target, name));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function readCanonicalManifest(manifestPath, expectedSnapshotHash) {
|
|
175
|
+
const raw = readFileSync(manifestPath, "utf8");
|
|
176
|
+
const manifest = JSON.parse(raw);
|
|
177
|
+
if (raw !== `${JSON.stringify(manifest, null, 2)}\n`) {
|
|
178
|
+
throw new Error("codex_selection_snapshot_manifest_not_canonical");
|
|
179
|
+
}
|
|
180
|
+
const { snapshotHash, ...unsigned } = manifest;
|
|
181
|
+
if (snapshotHash !== canonicalDigest(unsigned) || snapshotHash !== expectedSnapshotHash) {
|
|
182
|
+
throw new Error("codex_selection_snapshot_hash_mismatch");
|
|
183
|
+
}
|
|
184
|
+
for (const item of manifest.items ?? []) {
|
|
185
|
+
if (treeHash(item.backupPath) !== item.hash) {
|
|
186
|
+
throw new Error(`codex_selection_snapshot_backup_mismatch:${item.name}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return manifest;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function stateItems(paths, stableVersion) {
|
|
193
|
+
return [
|
|
194
|
+
{ name: "config", sourcePath: paths.configPath },
|
|
195
|
+
{ name: "marketplace", sourcePath: paths.marketplacePath },
|
|
196
|
+
{ name: "pluginRoot", sourcePath: paths.pluginRoot },
|
|
197
|
+
{ name: "stableCache", sourcePath: join(paths.cacheRoot, stableVersion) },
|
|
198
|
+
];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function stateHash(paths, stableVersion) {
|
|
202
|
+
return canonicalDigest(
|
|
203
|
+
stateItems(paths, stableVersion).map((item) => ({
|
|
204
|
+
name: item.name,
|
|
205
|
+
sourcePath: item.sourcePath,
|
|
206
|
+
hash: treeHash(item.sourcePath),
|
|
207
|
+
}))
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function snapshotCodexPluginSelection({ paths, snapshotRoot }) {
|
|
212
|
+
if (!paths || !snapshotRoot) throw new Error("codex_selection_snapshot_arguments_required");
|
|
213
|
+
if (existsSync(snapshotRoot)) throw new Error("codex_selection_snapshot_root_exists");
|
|
214
|
+
const version = pluginVersion(paths.pluginRoot);
|
|
215
|
+
const items = stateItems(paths, version).map((item) => {
|
|
216
|
+
if (!existsSync(item.sourcePath)) {
|
|
217
|
+
throw new Error(`codex_selection_snapshot_source_missing:${item.name}`);
|
|
218
|
+
}
|
|
219
|
+
const backupPath = join(snapshotRoot, "data", item.name);
|
|
220
|
+
copyExact(item.sourcePath, backupPath);
|
|
221
|
+
return { ...item, backupPath, hash: treeHash(item.sourcePath) };
|
|
222
|
+
});
|
|
223
|
+
const unsigned = {
|
|
224
|
+
schemaVersion: CODEX_PLUGIN_SELECTION_SCHEMA_VERSION,
|
|
225
|
+
createdAt: new Date().toISOString(),
|
|
226
|
+
pluginVersion: version,
|
|
227
|
+
stateHash: stateHash(paths, version),
|
|
228
|
+
paths,
|
|
229
|
+
items,
|
|
230
|
+
};
|
|
231
|
+
const manifest = { ...unsigned, snapshotHash: canonicalDigest(unsigned) };
|
|
232
|
+
const manifestPath = join(snapshotRoot, "snapshot.json");
|
|
233
|
+
mkdirSync(dirname(manifestPath), { recursive: true });
|
|
234
|
+
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
235
|
+
return { manifestPath, snapshotHash: manifest.snapshotHash, pluginVersion: version };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function installCodexCandidateCache({ paths, sourcePluginRoot, version }) {
|
|
239
|
+
if (!paths || !sourcePluginRoot || !version) throw new Error("codex_candidate_cache_arguments_required");
|
|
240
|
+
if (pluginVersion(sourcePluginRoot) !== version) throw new Error("codex_candidate_version_mismatch");
|
|
241
|
+
const target = join(paths.cacheRoot, version);
|
|
242
|
+
ensureInside(paths.cacheRoot, target, "codex_candidate_cache_path_escape");
|
|
243
|
+
const sourceHash = treeHash(sourcePluginRoot);
|
|
244
|
+
if (existsSync(target)) {
|
|
245
|
+
if (treeHash(target) !== sourceHash) throw new Error("codex_candidate_cache_collision");
|
|
246
|
+
} else {
|
|
247
|
+
mkdirSync(paths.cacheRoot, { recursive: true });
|
|
248
|
+
const staging = join(paths.cacheRoot, `.phase98-${version}-${process.pid}`);
|
|
249
|
+
copyExact(sourcePluginRoot, staging);
|
|
250
|
+
if (treeHash(staging) !== sourceHash) throw new Error("codex_candidate_cache_copy_mismatch");
|
|
251
|
+
renameSync(staging, target);
|
|
252
|
+
}
|
|
253
|
+
return { version, cachePath: target, cacheHash: sourceHash };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function withSelectionLock(paths, fn) {
|
|
257
|
+
mkdirSync(paths.cacheRoot, { recursive: true });
|
|
258
|
+
const lockPath = join(paths.cacheRoot, ".phase98-selection.lock");
|
|
259
|
+
let handle;
|
|
260
|
+
try {
|
|
261
|
+
handle = openSync(lockPath, "wx", 0o600);
|
|
262
|
+
} catch {
|
|
263
|
+
throw new Error("codex_selection_concurrent_operation");
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
return fn();
|
|
267
|
+
} finally {
|
|
268
|
+
closeSync(handle);
|
|
269
|
+
rmSync(lockPath, { force: true });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function atomicReplaceTree(source, target) {
|
|
274
|
+
const parent = dirname(target);
|
|
275
|
+
mkdirSync(parent, { recursive: true });
|
|
276
|
+
const staging = join(parent, `.${basename(target)}.phase98-next-${process.pid}`);
|
|
277
|
+
const previous = join(parent, `.${basename(target)}.phase98-prev-${process.pid}`);
|
|
278
|
+
copyExact(source, staging);
|
|
279
|
+
if (existsSync(target)) renameSync(target, previous);
|
|
280
|
+
try {
|
|
281
|
+
renameSync(staging, target);
|
|
282
|
+
rmSync(previous, { recursive: true, force: true });
|
|
283
|
+
} catch (error) {
|
|
284
|
+
rmSync(target, { recursive: true, force: true });
|
|
285
|
+
if (existsSync(previous)) renameSync(previous, target);
|
|
286
|
+
rmSync(staging, { recursive: true, force: true });
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function atomicReplaceFile(content, target) {
|
|
292
|
+
const parent = dirname(target);
|
|
293
|
+
mkdirSync(parent, { recursive: true });
|
|
294
|
+
const staging = join(parent, `.${basename(target)}.phase98-next-${process.pid}`);
|
|
295
|
+
const mode = existsSync(target) ? lstatSync(target).mode & 0o777 : 0o600;
|
|
296
|
+
writeFileSync(staging, content, { mode });
|
|
297
|
+
try {
|
|
298
|
+
renameSync(staging, target);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
rmSync(staging, { force: true });
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function activateCodexCandidateSelection(input) {
|
|
306
|
+
const {
|
|
307
|
+
paths,
|
|
308
|
+
snapshotManifestPath,
|
|
309
|
+
expectedSnapshotHash,
|
|
310
|
+
candidateVersion,
|
|
311
|
+
expectedCandidateHash,
|
|
312
|
+
approvalId,
|
|
313
|
+
approvedAt,
|
|
314
|
+
restoreBy,
|
|
315
|
+
now,
|
|
316
|
+
} = input;
|
|
317
|
+
if (!approvalId) throw new Error("codex_selection_approval_required");
|
|
318
|
+
const approvedMs = Date.parse(approvedAt);
|
|
319
|
+
const restoreMs = Date.parse(restoreBy);
|
|
320
|
+
const nowMs = Date.parse(now);
|
|
321
|
+
if (![approvedMs, restoreMs, nowMs].every(Number.isFinite)) {
|
|
322
|
+
throw new Error("codex_selection_window_invalid");
|
|
323
|
+
}
|
|
324
|
+
if (nowMs < approvedMs || nowMs >= restoreMs || restoreMs - approvedMs > MAX_CODEX_CANARY_WINDOW_MS) {
|
|
325
|
+
throw new Error("codex_selection_window_exceeds_limit");
|
|
326
|
+
}
|
|
327
|
+
return withSelectionLock(paths, () => {
|
|
328
|
+
const snapshot = readCanonicalManifest(snapshotManifestPath, expectedSnapshotHash);
|
|
329
|
+
if (stateHash(paths, snapshot.pluginVersion) !== snapshot.stateHash) {
|
|
330
|
+
throw new Error("codex_selection_current_state_drift");
|
|
331
|
+
}
|
|
332
|
+
const candidatePath = join(paths.cacheRoot, candidateVersion);
|
|
333
|
+
if (!existsSync(candidatePath) || treeHash(candidatePath) !== expectedCandidateHash) {
|
|
334
|
+
throw new Error("codex_selection_candidate_cache_mismatch");
|
|
335
|
+
}
|
|
336
|
+
const candidateConfig = candidateCodexConfig(
|
|
337
|
+
readFileSync(paths.configPath, "utf8"),
|
|
338
|
+
candidatePath
|
|
339
|
+
);
|
|
340
|
+
const configBackup = snapshot.items.find((item) => item.name === "config");
|
|
341
|
+
const pluginBackup = snapshot.items.find((item) => item.name === "pluginRoot");
|
|
342
|
+
if (!configBackup || !pluginBackup) {
|
|
343
|
+
throw new Error("codex_selection_snapshot_restore_items_missing");
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
atomicReplaceFile(candidateConfig, paths.configPath);
|
|
347
|
+
atomicReplaceTree(candidatePath, paths.pluginRoot);
|
|
348
|
+
if (
|
|
349
|
+
pluginVersion(paths.pluginRoot) !== candidateVersion ||
|
|
350
|
+
treeHash(paths.pluginRoot) !== expectedCandidateHash
|
|
351
|
+
) {
|
|
352
|
+
throw new Error("codex_selection_candidate_activation_mismatch");
|
|
353
|
+
}
|
|
354
|
+
} catch (error) {
|
|
355
|
+
copyExact(configBackup.backupPath, configBackup.sourcePath);
|
|
356
|
+
copyExact(pluginBackup.backupPath, pluginBackup.sourcePath);
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
const candidateConfigHash = treeHash(paths.configPath);
|
|
360
|
+
const activation = {
|
|
361
|
+
schemaVersion: CODEX_PLUGIN_SELECTION_SCHEMA_VERSION,
|
|
362
|
+
approvalId,
|
|
363
|
+
approvedAt,
|
|
364
|
+
activatedAt: now,
|
|
365
|
+
restoreBy,
|
|
366
|
+
snapshotHash: expectedSnapshotHash,
|
|
367
|
+
stableVersion: snapshot.pluginVersion,
|
|
368
|
+
candidateVersion,
|
|
369
|
+
candidateHash: expectedCandidateHash,
|
|
370
|
+
candidateConfigHash,
|
|
371
|
+
changedPaths: [paths.pluginRoot, paths.configPath],
|
|
372
|
+
};
|
|
373
|
+
const activationPath = join(dirname(snapshotManifestPath), "activation.json");
|
|
374
|
+
writeFileSync(activationPath, `${JSON.stringify(activation, null, 2)}\n`, { mode: 0o600 });
|
|
375
|
+
return { ...activation, activationPath };
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function restoreCodexPluginSelection(input) {
|
|
380
|
+
const {
|
|
381
|
+
paths,
|
|
382
|
+
snapshotManifestPath,
|
|
383
|
+
expectedSnapshotHash,
|
|
384
|
+
activationPath,
|
|
385
|
+
now,
|
|
386
|
+
taskEvidence,
|
|
387
|
+
recoveryReason,
|
|
388
|
+
} = input;
|
|
389
|
+
return withSelectionLock(paths, () => {
|
|
390
|
+
const snapshot = readCanonicalManifest(snapshotManifestPath, expectedSnapshotHash);
|
|
391
|
+
const activation = JSON.parse(readFileSync(activationPath, "utf8"));
|
|
392
|
+
if (
|
|
393
|
+
activation.snapshotHash !== expectedSnapshotHash ||
|
|
394
|
+
activation.candidateHash !== treeHash(paths.pluginRoot)
|
|
395
|
+
) throw new Error("codex_selection_active_candidate_drift");
|
|
396
|
+
if (
|
|
397
|
+
!activation.candidateConfigHash ||
|
|
398
|
+
activation.candidateConfigHash !== treeHash(paths.configPath)
|
|
399
|
+
) {
|
|
400
|
+
throw new Error("codex_selection_active_config_drift");
|
|
401
|
+
}
|
|
402
|
+
if (!recoveryReason) {
|
|
403
|
+
if (!taskEvidence?.taskId || taskEvidence.resolvedCandidateHash !== activation.candidateHash) {
|
|
404
|
+
throw new Error("codex_selection_task_identity_mismatch");
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
for (const item of snapshot.items) copyExact(item.backupPath, item.sourcePath);
|
|
408
|
+
if (stateHash(paths, snapshot.pluginVersion) !== snapshot.stateHash) {
|
|
409
|
+
throw new Error("codex_selection_restore_hash_mismatch");
|
|
410
|
+
}
|
|
411
|
+
const deadlineExceeded = Date.parse(now) > Date.parse(activation.restoreBy);
|
|
412
|
+
const restoration = {
|
|
413
|
+
restored: true,
|
|
414
|
+
restoredAt: now,
|
|
415
|
+
deadlineExceeded,
|
|
416
|
+
recoveryReason: recoveryReason || null,
|
|
417
|
+
snapshotHash: expectedSnapshotHash,
|
|
418
|
+
stableVersion: snapshot.pluginVersion,
|
|
419
|
+
candidateVersion: activation.candidateVersion,
|
|
420
|
+
};
|
|
421
|
+
writeFileSync(
|
|
422
|
+
join(dirname(snapshotManifestPath), "restoration.json"),
|
|
423
|
+
`${JSON.stringify(restoration, null, 2)}\n`,
|
|
424
|
+
{ mode: 0o600 }
|
|
425
|
+
);
|
|
426
|
+
return restoration;
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function readCodexPluginSelection({ paths, stableVersion } = {}) {
|
|
431
|
+
if (!paths) throw new Error("codex_selection_paths_required");
|
|
432
|
+
const version = pluginVersion(paths.pluginRoot);
|
|
433
|
+
const stable = stableVersion || version;
|
|
434
|
+
return {
|
|
435
|
+
pluginVersion: version,
|
|
436
|
+
pluginHash: treeHash(paths.pluginRoot),
|
|
437
|
+
configHash: treeHash(paths.configPath),
|
|
438
|
+
marketplaceHash: treeHash(paths.marketplacePath),
|
|
439
|
+
stableCacheHash: treeHash(join(paths.cacheRoot, stable)),
|
|
440
|
+
selectionHash: stateHash(paths, stable),
|
|
441
|
+
};
|
|
442
|
+
}
|
package/lib/runtime-verify.mjs
CHANGED
|
@@ -11,25 +11,13 @@ export const REQUIRED_SELLABLE_MCP_TOOLS = [
|
|
|
11
11
|
"start_cli_login",
|
|
12
12
|
"wait_for_cli_login",
|
|
13
13
|
"bootstrap_create_campaign",
|
|
14
|
-
"bootstrap_find_leads",
|
|
15
|
-
"get_find_leads_run",
|
|
16
|
-
"update_find_leads_run",
|
|
17
|
-
"wait_for_find_leads_run",
|
|
18
|
-
"cancel_find_leads",
|
|
19
|
-
"reissue_find_leads_watch_link",
|
|
20
|
-
"preflight_find_leads_provider",
|
|
21
14
|
"get_subskill_prompt",
|
|
22
15
|
"get_subskill_asset",
|
|
23
16
|
"search_subskill_prompts",
|
|
24
17
|
"create_campaign",
|
|
25
18
|
"import_leads",
|
|
26
|
-
"search_sales_nav",
|
|
27
|
-
"search_prospeo",
|
|
28
|
-
"search_signals",
|
|
29
|
-
"select_promising_posts",
|
|
30
19
|
"confirm_lead_list",
|
|
31
20
|
"wait_for_lead_list_ready",
|
|
32
|
-
"export_table_csv",
|
|
33
21
|
"wait_for_campaign_table_ready",
|
|
34
22
|
"update_campaign",
|
|
35
23
|
"update_campaign_brief",
|
|
@@ -88,14 +76,6 @@ export const CREATE_CAMPAIGN_SMOKE_CALLS = [
|
|
|
88
76
|
limit: 1200,
|
|
89
77
|
},
|
|
90
78
|
},
|
|
91
|
-
{
|
|
92
|
-
name: "get_subskill_prompt",
|
|
93
|
-
arguments: {
|
|
94
|
-
subskillName: "find-leads-v2",
|
|
95
|
-
offset: 0,
|
|
96
|
-
limit: 1200,
|
|
97
|
-
},
|
|
98
|
-
},
|
|
99
79
|
{
|
|
100
80
|
name: "get_subskill_prompt",
|
|
101
81
|
arguments: {
|
|
@@ -137,15 +117,6 @@ export const CREATE_CAMPAIGN_SMOKE_CALLS = [
|
|
|
137
117
|
limit: 1200,
|
|
138
118
|
},
|
|
139
119
|
},
|
|
140
|
-
{
|
|
141
|
-
name: "get_subskill_asset",
|
|
142
|
-
arguments: {
|
|
143
|
-
subskillName: "find-leads-v2",
|
|
144
|
-
assetPath: "core/flow.v1.json",
|
|
145
|
-
offset: 0,
|
|
146
|
-
limit: 1200,
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
120
|
];
|
|
150
121
|
|
|
151
122
|
function collectStderrLines(stream) {
|
|
@@ -372,7 +343,7 @@ async function verifyCreateCampaignSmoke({
|
|
|
372
343
|
ok: false,
|
|
373
344
|
missingTools,
|
|
374
345
|
calls,
|
|
375
|
-
error: `Missing
|
|
346
|
+
error: `Missing smoke tool(s): ${missingTools.join(", ")}`,
|
|
376
347
|
startedAt,
|
|
377
348
|
completedAt: new Date().toISOString(),
|
|
378
349
|
};
|
|
@@ -691,7 +662,7 @@ async function verifySellableMcpRuntimeOnce({
|
|
|
691
662
|
),
|
|
692
663
|
calls: [],
|
|
693
664
|
error:
|
|
694
|
-
"
|
|
665
|
+
"Skipping create-campaign smoke because required MCP tools are missing.",
|
|
695
666
|
startedAt: new Date().toISOString(),
|
|
696
667
|
completedAt: new Date().toISOString(),
|
|
697
668
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sellable/install",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.329",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"lib",
|
|
16
16
|
"agents",
|
|
17
17
|
"skill-templates",
|
|
18
|
-
"README.md"
|
|
18
|
+
"README.md",
|
|
19
|
+
"HERMES-SLACK-PROFILE-SCOPING.md"
|
|
19
20
|
],
|
|
20
21
|
"dependencies": {
|
|
21
22
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
@@ -721,7 +721,7 @@ Treat host capabilities as concrete functions, not prose conventions:
|
|
|
721
721
|
import and before dispatching Message Drafting only.
|
|
722
722
|
- `launch_message_drafting`: Claude Code uses `Task` with `subagent_type`
|
|
723
723
|
`post-find-leads-message-scout` when listed; Codex uses the returned
|
|
724
|
-
compatibility agent or a generic `gpt-5.
|
|
724
|
+
compatibility agent or a generic `gpt-5.6-sol` / `high` Message Drafting agent.
|
|
725
725
|
|
|
726
726
|
If a required interactive question function or MCP loader is missing, stop and
|
|
727
727
|
explain the Sellable install/reload problem. Source work uses product-native MCP
|
|
@@ -1082,8 +1082,8 @@ updates.
|
|
|
1082
1082
|
In Codex, the filter-choice answer is the campaign-scoped go-ahead to use
|
|
1083
1083
|
this single Message Drafting background agent in step-wise and YOLO modes.
|
|
1084
1084
|
Do not ask a separate question to start it. If the named custom agent is not
|
|
1085
|
-
available, spawn a generic background agent with `model: "gpt-5.
|
|
1086
|
-
`reasoning_effort: "
|
|
1085
|
+
available, spawn a generic background agent with `model: "gpt-5.6-sol"` and
|
|
1086
|
+
`reasoning_effort: "high"` using the same lean campaign/table basis. If no
|
|
1087
1087
|
background-agent tool is callable, start the same full message branch inline
|
|
1088
1088
|
before filter drafting or skip-filter message review and record it as
|
|
1089
1089
|
`statusSource: "parent-thread-fallback"`.
|
|
@@ -437,7 +437,7 @@ fallback only when app thread tools are unavailable; the receipt must include
|
|
|
437
437
|
`fallbackReason`, or backend verify rejects it.
|
|
438
438
|
When visible Codex app thread tools are unavailable but local Codex CLI is
|
|
439
439
|
available, the accepted durable streaming-worker command shape is:
|
|
440
|
-
`codex -a never -s danger-full-access -c model_reasoning_effort=
|
|
440
|
+
`codex -a never -s danger-full-access -c model_reasoning_effort=high exec --skip-git-repo-check -m <worker-model> -C <repo> -o <worker-final-file> -`.
|
|
441
441
|
The approval, sandbox, and reasoning-effort config flags must come before the
|
|
442
442
|
`exec` subcommand for Codex CLI builds that expose `-a`/`-s`/`-c` only at top
|
|
443
443
|
level. `--skip-git-repo-check` belongs after `exec` because current customer and VPS Codex CLI builds expose it
|
|
@@ -447,26 +447,26 @@ forms fail on current customer CLI installs. Pipe the lane packet prompt on
|
|
|
447
447
|
stdin, require the worker to write `workerDispatch.receiptArtifactHint`, and
|
|
448
448
|
pass exactly one lane packet per worker.
|
|
449
449
|
When launching durable Codex CLI workers from an automation parent, pass an
|
|
450
|
-
explicit supported worker model and explicit `
|
|
450
|
+
explicit supported worker model and explicit `high` reasoning effort instead
|
|
451
451
|
of relying on the Codex CLI defaults. Use the parent runtime model when known,
|
|
452
452
|
for example:
|
|
453
|
-
`codex -a never -s danger-full-access -c model_reasoning_effort=
|
|
453
|
+
`codex -a never -s danger-full-access -c model_reasoning_effort=high exec --skip-git-repo-check -m <worker-model> -C <repo> -o <worker-final-file> -`.
|
|
454
454
|
In Codex CLI, the parent runtime model is visible in the run header as
|
|
455
455
|
`model: <model-name>`. Copy that exact model string into child worker launches
|
|
456
|
-
first. If the parent header says `model: gpt-5.
|
|
457
|
-
`-m gpt-5.
|
|
456
|
+
first. If the parent header says `model: gpt-5.6-sol`, launch workers with
|
|
457
|
+
`-m gpt-5.6-sol` plus `-c model_reasoning_effort=high`; do not invent or probe
|
|
458
458
|
nearby aliases such as `gpt-5.3-codex`, `gpt-5.2`, `gpt-5-codex`,
|
|
459
459
|
`codex-latest`, or `codex-mini-latest` before trying the exact parent model. A
|
|
460
|
-
one-line probe must include the same `-c model_reasoning_effort=
|
|
460
|
+
one-line probe must include the same `-c model_reasoning_effort=high` override
|
|
461
461
|
and count as supported only when it exits 0 and returns the requested output; a
|
|
462
462
|
session header followed by a `not supported` error is rejected, not accepted.
|
|
463
463
|
Do not rely on the Codex CLI default model or default reasoning effort; some
|
|
464
|
-
customer and VPS installs default to unavailable model aliases or to
|
|
465
|
-
|
|
466
|
-
with `high
|
|
467
|
-
`
|
|
464
|
+
customer and VPS installs default to unavailable model aliases or to reasoning
|
|
465
|
+
below `high`. `-m gpt-5.6-sol` alone is not enough. If a child worker reports GPT 5.6-Sol
|
|
466
|
+
with reasoning below `high`, treat that as a launcher bug, relaunch with the explicit
|
|
467
|
+
`high` config before mutation, and do not ask the user to continue through the
|
|
468
468
|
model-quality warning. If the parent cannot identify a supported worker model
|
|
469
|
-
and launch it with `
|
|
469
|
+
and launch it with `high` reasoning, stop with
|
|
470
470
|
`blocked: worker_model_unavailable` before mutation.
|
|
471
471
|
When wrapping multiple local Codex CLI workers in a shell launcher, run the
|
|
472
472
|
wrapper with `/bin/bash -lc` or another explicitly chosen portable shell. Do not
|
|
@@ -479,7 +479,7 @@ Do not embed `<<'WORKER_PROMPT'` heredocs inside a single quoted or double
|
|
|
479
479
|
quoted `/bin/bash -lc '...'` command string; nested quoting is brittle and can
|
|
480
480
|
truncate the first worker before mutation. For multi-worker launchers, write one
|
|
481
481
|
plain prompt file per lane under the current run directory, then start each
|
|
482
|
-
worker with `codex -a never -s danger-full-access -c model_reasoning_effort=
|
|
482
|
+
worker with `codex -a never -s danger-full-access -c model_reasoning_effort=high exec --skip-git-repo-check -m "$WORKER_MODEL" -C "$REPO" -o
|
|
483
483
|
"$worker_final_file" - < "$worker_prompt_file"`. Keep launcher shell variables
|
|
484
484
|
double-quoted and keep the prompt heredoc only in a standalone script/prompt-file
|
|
485
485
|
write step, not inside an already quoted shell argument. If the first launcher
|
|
@@ -585,7 +585,7 @@ launchers, write the same prompt body to `<worker-prompt-file>` and launch the
|
|
|
585
585
|
worker with stdin redirected from that file:
|
|
586
586
|
|
|
587
587
|
```
|
|
588
|
-
codex -a never -s danger-full-access -c model_reasoning_effort=
|
|
588
|
+
codex -a never -s danger-full-access -c model_reasoning_effort=high exec --skip-git-repo-check -m <worker-model> -C <repo> -o <worker-final-file> - <<'WORKER_PROMPT'
|
|
589
589
|
Use $sellable:create-campaign as the governing campaign workflow for this one lane worker.
|
|
590
590
|
Use the evergreen plan/packet below only for lane scope, source metadata,
|
|
591
591
|
postconditions, side-effect caps, and durable receipt proof.
|
|
@@ -632,10 +632,10 @@ Do not launch, start, schedule, send, or use paid InMail.
|
|
|
632
632
|
For filter proof, durable receipt status must be `filterDecisionReceipt.status:"applied"` only; never `completed`, `confirmed`, `done`, or aliases.
|
|
633
633
|
For generated messages, `update_cell` is allowed only for the semantic Approved checkbox. Never use `update_cell` for generated message text/body/sample copy. Bad copy requires `revise_message_template_and_rerun` or brief/template revision plus Generate Message rerun. Any generated-message cell override is `blocked: generated_message_cell_override`.
|
|
634
634
|
If `bootstrap_create_campaign.modelQuality.status === "warn"` because the child
|
|
635
|
-
worker reports GPT 5.
|
|
635
|
+
worker reports GPT 5.6-Sol with reasoning below `high`, that is a parent launcher
|
|
636
636
|
configuration bug, not an operator approval path inside the worker. Stop before
|
|
637
637
|
mutation, tell the parent to relaunch this lane with
|
|
638
|
-
`-c model_reasoning_effort=
|
|
638
|
+
`-c model_reasoning_effort=high`, and do not mark the worker goal complete.
|
|
639
639
|
Complete only this lane. Do not end with narration only. Before your final
|
|
640
640
|
response, run a local file-existence and JSON self-check for
|
|
641
641
|
<receiptArtifactPath>. If the lane succeeded, write the canonical success
|
|
@@ -656,7 +656,7 @@ receipt, or an accepted Post Engagers no-source blocked/no-op receipt.
|
|
|
656
656
|
WORKER_PROMPT
|
|
657
657
|
|
|
658
658
|
# Multi-worker launcher equivalent:
|
|
659
|
-
codex -a never -s danger-full-access -c model_reasoning_effort=
|
|
659
|
+
codex -a never -s danger-full-access -c model_reasoning_effort=high exec --skip-git-repo-check -m <worker-model> -C <repo> -o <worker-final-file> - < <worker-prompt-file>
|
|
660
660
|
```
|
|
661
661
|
|
|
662
662
|
If any placeholder cannot be filled from the current plan, matching
|
|
@@ -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:
|
|
4
|
+
visibility: internal
|
|
5
5
|
allowed-tools:
|
|
6
|
-
-
|
|
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
|
-
`
|
|
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
|
-
|
|
43
|
+
refill_sends({ workspaceId, intent:"auto", senderIds?, approvalMode? })
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
For read-only inspection:
|
|
47
47
|
|
|
48
48
|
```text
|
|
49
|
-
|
|
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
|
-
|
|
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
|