@cohortapp/agent-sdk 2.6.0 → 2.6.1
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/maestro.mjs +120 -1
- package/bin/maestro.test.mjs +182 -0
- package/package.json +1 -1
- package/scripts/daemon/classifier.test.mjs +18 -9
package/bin/maestro.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
readdirSync,
|
|
22
22
|
statSync,
|
|
23
23
|
lstatSync,
|
|
24
|
+
unlinkSync,
|
|
24
25
|
openSync,
|
|
25
26
|
readSync,
|
|
26
27
|
closeSync,
|
|
@@ -767,6 +768,23 @@ const UPGRADE_PATHS = [
|
|
|
767
768
|
{ path: "archetypes", mode: "smart" },
|
|
768
769
|
];
|
|
769
770
|
|
|
771
|
+
// Directories that sit under a "smart" upgrade root but hold MACHINE-GENERATED
|
|
772
|
+
// artifacts rather than framework files — launchd plists rendered per-machine
|
|
773
|
+
// from config/agent.json. Upstream ships no copy of them by design, so to the
|
|
774
|
+
// prune pass they look exactly like orphans: tracked, clean, and absent from
|
|
775
|
+
// the SDK. Git cannot tell the difference, because there is no difference to
|
|
776
|
+
// see — the intent lives in the directory, not the file state. Deleting them
|
|
777
|
+
// would destroy this machine's schedule (and would race the cadence-bus
|
|
778
|
+
// migration, which backs these same plists up and regenerates them).
|
|
779
|
+
const PRUNE_EXCLUDE_PREFIXES = [
|
|
780
|
+
"scripts/local-triggers/plists/",
|
|
781
|
+
"scripts/poller-launchd/",
|
|
782
|
+
];
|
|
783
|
+
|
|
784
|
+
function isPruneExcluded(repoRel) {
|
|
785
|
+
return PRUNE_EXCLUDE_PREFIXES.some((p) => repoRel.startsWith(p));
|
|
786
|
+
}
|
|
787
|
+
|
|
770
788
|
function sha256File(p) {
|
|
771
789
|
return createHash("sha256").update(readFileSync(p)).digest("hex");
|
|
772
790
|
}
|
|
@@ -1074,11 +1092,13 @@ function parseUpgradeFlags(args) {
|
|
|
1074
1092
|
forceOverwrite: false,
|
|
1075
1093
|
noIncoming: false,
|
|
1076
1094
|
verbose: false,
|
|
1095
|
+
noPrune: false,
|
|
1077
1096
|
};
|
|
1078
1097
|
for (const a of args) {
|
|
1079
1098
|
if (a === "--dry-run" || a === "-n") flags.dryRun = true;
|
|
1080
1099
|
else if (a === "--force-overwrite" || a === "--force") flags.forceOverwrite = true;
|
|
1081
1100
|
else if (a === "--no-incoming") flags.noIncoming = true;
|
|
1101
|
+
else if (a === "--no-prune") flags.noPrune = true;
|
|
1082
1102
|
else if (a === "--verbose" || a === "-v") flags.verbose = true;
|
|
1083
1103
|
else if (a === "--help" || a === "-h") return null;
|
|
1084
1104
|
else { fail(`Unknown flag: ${a}`); process.exit(1); }
|
|
@@ -1096,6 +1116,7 @@ Flags:
|
|
|
1096
1116
|
--dry-run, -n Preview changes without writing
|
|
1097
1117
|
--force-overwrite Overwrite even locally-modified files (backs them up)
|
|
1098
1118
|
--no-incoming Don't write .maestro/incoming/ shadows for preserved files
|
|
1119
|
+
--no-prune Keep framework files that upstream has deleted
|
|
1099
1120
|
--verbose, -v Print classification for every file
|
|
1100
1121
|
--help, -h Show this help
|
|
1101
1122
|
|
|
@@ -1108,6 +1129,9 @@ Per-file behaviour:
|
|
|
1108
1129
|
.maestro/incoming/<path> for manual diff
|
|
1109
1130
|
mergeKept — under agents/ → never overwrite (custom agents preserved)
|
|
1110
1131
|
forced — overwritten by --force-overwrite; backup at .maestro/backup/<path>
|
|
1132
|
+
pruned — deleted upstream and pristine here → removed; backup at
|
|
1133
|
+
.maestro/backup/<path>. Only ever applies to files git reports as
|
|
1134
|
+
tracked-and-clean, so your own files are never at risk.
|
|
1111
1135
|
|
|
1112
1136
|
.maestroignore format (gitignore-style, top-down, last match wins):
|
|
1113
1137
|
scripts/slack-send.sh exact file
|
|
@@ -1145,9 +1169,11 @@ Per-file behaviour:
|
|
|
1145
1169
|
const banner = flags.dryRun ? "DRY RUN — " : "";
|
|
1146
1170
|
log(`${banner}Upgrading framework files from @cohortapp/agent-sdk...`);
|
|
1147
1171
|
|
|
1148
|
-
const counts = { added: 0, updated: 0, same: 0, ignored: 0, preserved: 0, mergeKept: 0, forced: 0 };
|
|
1172
|
+
const counts = { added: 0, updated: 0, same: 0, ignored: 0, preserved: 0, mergeKept: 0, forced: 0, pruned: 0, pruneKept: 0 };
|
|
1149
1173
|
const preservedFiles = [];
|
|
1150
1174
|
const ignoredFiles = [];
|
|
1175
|
+
const prunedFiles = [];
|
|
1176
|
+
const pruneKeptFiles = [];
|
|
1151
1177
|
|
|
1152
1178
|
for (const { path: relRoot, mode } of UPGRADE_PATHS) {
|
|
1153
1179
|
const srcRoot = join(MAESTRO_ROOT, relRoot);
|
|
@@ -1235,6 +1261,86 @@ Per-file behaviour:
|
|
|
1235
1261
|
}
|
|
1236
1262
|
}
|
|
1237
1263
|
|
|
1264
|
+
// ── Prune: framework files this agent still holds but upstream deleted ─────
|
|
1265
|
+
//
|
|
1266
|
+
// The copy loop above is one-directional: it adds and overwrites, but never
|
|
1267
|
+
// removes. So every file the SDK has ever shipped stays resident on an agent
|
|
1268
|
+
// machine forever. That is not merely untidy — an orphan is a *stale module*
|
|
1269
|
+
// that keeps importing symbols its collaborators no longer export, so it
|
|
1270
|
+
// fails permanently and drags the agent's own test suite red. (Found in the
|
|
1271
|
+
// field: scripts/daemon/responder.test.mjs survived the transport refactor
|
|
1272
|
+
// and kept importing a deleted `loadConversationHistory`.)
|
|
1273
|
+
//
|
|
1274
|
+
// Prune asks the converse of the copy loop's question — "does the agent hold
|
|
1275
|
+
// a file upstream deleted?" — and reuses the identical safety rules:
|
|
1276
|
+
//
|
|
1277
|
+
// * .maestroignore still wins; an ignored path is never touched.
|
|
1278
|
+
// * "merge" roots (agents/) are skipped wholesale — files there are
|
|
1279
|
+
// *supposed* to exist only locally; that is the mode's entire purpose.
|
|
1280
|
+
// * Only files git reports as tracked-and-clean are removed. dirtyPathSet()
|
|
1281
|
+
// is built with --untracked-files=all, so a path absent from it is
|
|
1282
|
+
// provably a pristine framework file the operator never touched.
|
|
1283
|
+
// Anything else — edited, untracked, operator-authored — is kept and
|
|
1284
|
+
// reported, never deleted.
|
|
1285
|
+
// * Outside a git repo we cannot prove any of the above, so prune does
|
|
1286
|
+
// nothing at all rather than guess.
|
|
1287
|
+
//
|
|
1288
|
+
// Removals are backed up under .maestro/backup/ exactly like a forced
|
|
1289
|
+
// overwrite, so a prune is always reversible.
|
|
1290
|
+
if (flags.noPrune) {
|
|
1291
|
+
log("Prune skipped (--no-prune): upstream-deleted files left in place.");
|
|
1292
|
+
} else if (!inGit) {
|
|
1293
|
+
warn("Prune skipped: not a git repo, so pristine framework files can't be told from yours.");
|
|
1294
|
+
} else {
|
|
1295
|
+
for (const { path: relRoot, mode } of UPGRADE_PATHS) {
|
|
1296
|
+
// merge-mode roots hold deliberately-local files — nothing to reconcile.
|
|
1297
|
+
if (mode === "merge") continue;
|
|
1298
|
+
const srcRoot = join(MAESTRO_ROOT, relRoot);
|
|
1299
|
+
const dstRoot = join(cwd, relRoot);
|
|
1300
|
+
// If upstream dropped the whole root, treat it as out of scope rather
|
|
1301
|
+
// than deleting an entire directory tree on the agent's machine.
|
|
1302
|
+
if (!existsSync(srcRoot) || !existsSync(dstRoot)) continue;
|
|
1303
|
+
|
|
1304
|
+
for (const dstFile of walkFiles(dstRoot)) {
|
|
1305
|
+
const relFromRoot = relative(dstRoot, dstFile);
|
|
1306
|
+
if (existsSync(join(srcRoot, relFromRoot))) continue; // still shipped
|
|
1307
|
+
|
|
1308
|
+
const repoRel = relative(cwd, dstFile).split(sep).join("/");
|
|
1309
|
+
|
|
1310
|
+
// Machine-generated, not framework-shipped — see PRUNE_EXCLUDE_PREFIXES.
|
|
1311
|
+
if (isPruneExcluded(repoRel)) {
|
|
1312
|
+
if (flags.verbose) console.log(` · ${repoRel} (machine-generated, never pruned)`);
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
if (matchesIgnore(repoRel, ignorePatterns)) {
|
|
1317
|
+
counts.ignored++;
|
|
1318
|
+
ignoredFiles.push(repoRel);
|
|
1319
|
+
if (flags.verbose) console.log(` · ${repoRel} (orphan, ignored via .maestroignore)`);
|
|
1320
|
+
continue;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// Dirty covers edited AND untracked — either way it isn't ours to delete.
|
|
1324
|
+
if (dirty.has(repoRel)) {
|
|
1325
|
+
counts.pruneKept++;
|
|
1326
|
+
pruneKeptFiles.push(repoRel);
|
|
1327
|
+
if (flags.verbose) console.log(` ~ ${repoRel} (deleted upstream, but yours — kept)`);
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
if (!flags.dryRun) {
|
|
1332
|
+
const backup = join(cwd, ".maestro", "backup", repoRel);
|
|
1333
|
+
mkdirSync(dirname(backup), { recursive: true });
|
|
1334
|
+
copyFileSync(dstFile, backup);
|
|
1335
|
+
unlinkSync(dstFile);
|
|
1336
|
+
}
|
|
1337
|
+
counts.pruned++;
|
|
1338
|
+
prunedFiles.push(repoRel);
|
|
1339
|
+
if (flags.verbose) warn(`- ${repoRel} (deleted upstream; backup written)`);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1238
1344
|
// Ensure standard runtime directories exist.
|
|
1239
1345
|
const ensureDirs = [
|
|
1240
1346
|
"state/handoffs", "state/huddle", "state/indexes", "state/rag",
|
|
@@ -1383,8 +1489,21 @@ Per-file behaviour:
|
|
|
1383
1489
|
if (counts.mergeKept) console.log(` ~ ${counts.mergeKept} merge-mode kept (agents/ custom files preserved)`);
|
|
1384
1490
|
if (counts.preserved) warn(`${counts.preserved} preserved (local edits — kept your version)`);
|
|
1385
1491
|
if (counts.forced) warn(`${counts.forced} force-overwritten (backups in .maestro/backup/)`);
|
|
1492
|
+
if (counts.pruned) ok(`${counts.pruned} pruned (deleted upstream; backups in .maestro/backup/)`);
|
|
1493
|
+
if (counts.pruneKept) warn(`${counts.pruneKept} deleted upstream but kept (yours — edited or untracked)`);
|
|
1386
1494
|
if (newDirs) ok(`${newDirs} new directories created`);
|
|
1387
1495
|
|
|
1496
|
+
if (prunedFiles.length && flags.verbose === false) {
|
|
1497
|
+
for (const p of prunedFiles.slice(0, 5)) console.log(` - ${p}`);
|
|
1498
|
+
if (prunedFiles.length > 5) console.log(` … and ${prunedFiles.length - 5} more`);
|
|
1499
|
+
}
|
|
1500
|
+
if (pruneKeptFiles.length) {
|
|
1501
|
+
console.log();
|
|
1502
|
+
log("These are gone from the framework but still present here — delete if you no longer want them:");
|
|
1503
|
+
for (const p of pruneKeptFiles.slice(0, 5)) console.log(` ${p}`);
|
|
1504
|
+
if (pruneKeptFiles.length > 5) console.log(` … and ${pruneKeptFiles.length - 5} more`);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1388
1507
|
if (preservedFiles.length && !flags.noIncoming && !flags.dryRun) {
|
|
1389
1508
|
console.log();
|
|
1390
1509
|
log("Upstream versions of your locally-modified files saved to .maestro/incoming/");
|
package/bin/maestro.test.mjs
CHANGED
|
@@ -1140,3 +1140,185 @@ test("doctor surfaces the new framework systems (org / secrets / diagnostics)",
|
|
|
1140
1140
|
await rmRoot(shim);
|
|
1141
1141
|
}
|
|
1142
1142
|
});
|
|
1143
|
+
|
|
1144
|
+
// ---------------------------------------------------------------------------
|
|
1145
|
+
// upgrade — prune (framework files deleted upstream)
|
|
1146
|
+
//
|
|
1147
|
+
// The copy loop only ever adds and overwrites, so before this every file the
|
|
1148
|
+
// SDK had ever shipped stayed resident on an agent machine forever. Orphaned
|
|
1149
|
+
// *modules* keep importing symbols their collaborators no longer export, so
|
|
1150
|
+
// they fail permanently and drag the agent's own suite red — found in the
|
|
1151
|
+
// field on Isla's machine, where a stale scripts/daemon/responder.test.mjs
|
|
1152
|
+
// survived the transport refactor still importing a deleted export.
|
|
1153
|
+
//
|
|
1154
|
+
// Prune's contract is narrow on purpose: it removes a file only when git
|
|
1155
|
+
// proves it is tracked-and-clean, i.e. pristine framework material the
|
|
1156
|
+
// operator never touched. Everything else is kept and reported.
|
|
1157
|
+
// ---------------------------------------------------------------------------
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* Agent repo carrying `orphans` — paths upstream does not ship. Written before
|
|
1161
|
+
* the commit so git sees them as tracked and clean (prune's precondition).
|
|
1162
|
+
*/
|
|
1163
|
+
async function makeAgentWithOrphans(orphans) {
|
|
1164
|
+
const root = await tmpRoot("maestro-orphan");
|
|
1165
|
+
mkdirSync(join(root, "config"), { recursive: true });
|
|
1166
|
+
writeFileSync(join(root, "CLAUDE.md"), "# agent\n");
|
|
1167
|
+
writeFileSync(join(root, "config/agent.ts"), "export const AGENT = { firstName: 'orphan' };\n");
|
|
1168
|
+
writeFileSync(join(root, "package.json"), '{"name":"orphan-agent","version":"1.0.0"}\n');
|
|
1169
|
+
for (const [rel, body] of Object.entries(orphans)) {
|
|
1170
|
+
mkdirSync(dirname(join(root, rel)), { recursive: true });
|
|
1171
|
+
writeFileSync(join(root, rel), body);
|
|
1172
|
+
}
|
|
1173
|
+
execFileSync("git", ["init", "-q"], { cwd: root });
|
|
1174
|
+
execFileSync("git", ["add", "-A"], { cwd: root });
|
|
1175
|
+
execFileSync(
|
|
1176
|
+
"git",
|
|
1177
|
+
["-c", "user.email=test@test", "-c", "user.name=test", "commit", "-q", "-m", "init"],
|
|
1178
|
+
{ cwd: root },
|
|
1179
|
+
);
|
|
1180
|
+
return root;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
const ORPHAN = "lib/zz-deleted-upstream-fixture.mjs";
|
|
1184
|
+
|
|
1185
|
+
test("upgrade prunes a pristine framework file deleted upstream, with a backup", async () => {
|
|
1186
|
+
const root = await makeAgentWithOrphans({ [ORPHAN]: "export const gone = 1;\n" });
|
|
1187
|
+
try {
|
|
1188
|
+
const r = runCli(["upgrade"], root);
|
|
1189
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1190
|
+
assert.ok(!existsSync(join(root, ORPHAN)), "orphan should have been pruned");
|
|
1191
|
+
assert.ok(
|
|
1192
|
+
existsSync(join(root, ".maestro/backup", ORPHAN)),
|
|
1193
|
+
"prune must be reversible — a backup is required",
|
|
1194
|
+
);
|
|
1195
|
+
assert.equal(
|
|
1196
|
+
readFileSync(join(root, ".maestro/backup", ORPHAN), "utf-8"),
|
|
1197
|
+
"export const gone = 1;\n",
|
|
1198
|
+
"backup must hold the original bytes",
|
|
1199
|
+
);
|
|
1200
|
+
} finally {
|
|
1201
|
+
await fsp.rm(root, { recursive: true, force: true });
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
test("upgrade never prunes a file the operator edited", async () => {
|
|
1206
|
+
const root = await makeAgentWithOrphans({ [ORPHAN]: "export const gone = 1;\n" });
|
|
1207
|
+
try {
|
|
1208
|
+
writeFileSync(join(root, ORPHAN), "export const gone = 2; // my change\n");
|
|
1209
|
+
const r = runCli(["upgrade"], root);
|
|
1210
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1211
|
+
assert.ok(existsSync(join(root, ORPHAN)), "an edited orphan is the operator's, not ours to delete");
|
|
1212
|
+
assert.match(
|
|
1213
|
+
readFileSync(join(root, ORPHAN), "utf-8"),
|
|
1214
|
+
/my change/,
|
|
1215
|
+
"the operator's bytes must survive verbatim",
|
|
1216
|
+
);
|
|
1217
|
+
assert.match(r.stdout, /kept/i, "the kept orphan must be reported, not silently left");
|
|
1218
|
+
} finally {
|
|
1219
|
+
await fsp.rm(root, { recursive: true, force: true });
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
test("upgrade never prunes an untracked file", async () => {
|
|
1224
|
+
const root = await makeAgentWithOrphans({});
|
|
1225
|
+
try {
|
|
1226
|
+
// Written after the commit → untracked → indistinguishable from operator work.
|
|
1227
|
+
mkdirSync(join(root, "lib"), { recursive: true });
|
|
1228
|
+
writeFileSync(join(root, ORPHAN), "export const mine = 1;\n");
|
|
1229
|
+
const r = runCli(["upgrade"], root);
|
|
1230
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1231
|
+
assert.ok(existsSync(join(root, ORPHAN)), "untracked files are never prune candidates");
|
|
1232
|
+
} finally {
|
|
1233
|
+
await fsp.rm(root, { recursive: true, force: true });
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
test("upgrade --no-prune leaves upstream-deleted files in place", async () => {
|
|
1238
|
+
const root = await makeAgentWithOrphans({ [ORPHAN]: "export const gone = 1;\n" });
|
|
1239
|
+
try {
|
|
1240
|
+
const r = runCli(["upgrade", "--no-prune"], root);
|
|
1241
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1242
|
+
assert.ok(existsSync(join(root, ORPHAN)), "--no-prune must suppress removal entirely");
|
|
1243
|
+
} finally {
|
|
1244
|
+
await fsp.rm(root, { recursive: true, force: true });
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
test("upgrade --dry-run reports a prune without performing it", async () => {
|
|
1249
|
+
const root = await makeAgentWithOrphans({ [ORPHAN]: "export const gone = 1;\n" });
|
|
1250
|
+
try {
|
|
1251
|
+
const r = runCli(["upgrade", "--dry-run"], root);
|
|
1252
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1253
|
+
assert.ok(existsSync(join(root, ORPHAN)), "dry run must not delete");
|
|
1254
|
+
assert.ok(!existsSync(join(root, ".maestro/backup", ORPHAN)), "dry run must not write backups");
|
|
1255
|
+
assert.match(r.stdout, /prune/i, "dry run must still tell the operator what it would remove");
|
|
1256
|
+
} finally {
|
|
1257
|
+
await fsp.rm(root, { recursive: true, force: true });
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
test("upgrade honours .maestroignore over prune", async () => {
|
|
1262
|
+
const root = await makeAgentWithOrphans({
|
|
1263
|
+
[ORPHAN]: "export const gone = 1;\n",
|
|
1264
|
+
".maestroignore": `${ORPHAN}\n`,
|
|
1265
|
+
});
|
|
1266
|
+
try {
|
|
1267
|
+
const r = runCli(["upgrade"], root);
|
|
1268
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1269
|
+
assert.ok(existsSync(join(root, ORPHAN)), ".maestroignore must win over prune, as it does over overwrite");
|
|
1270
|
+
} finally {
|
|
1271
|
+
await fsp.rm(root, { recursive: true, force: true });
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
test("upgrade prune leaves files that upstream still ships", async () => {
|
|
1276
|
+
const root = await makeAgentWithOrphans({});
|
|
1277
|
+
try {
|
|
1278
|
+
const r = runCli(["upgrade"], root);
|
|
1279
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1280
|
+
// A file the SDK genuinely ships must survive — the guard against a prune
|
|
1281
|
+
// that mistakes "present upstream" for "absent" and empties the repo.
|
|
1282
|
+
assert.ok(
|
|
1283
|
+
existsSync(join(root, "lib/identity/persona.mjs")),
|
|
1284
|
+
"currently-shipped framework files must never be pruned",
|
|
1285
|
+
);
|
|
1286
|
+
assert.ok(existsSync(join(root, "scripts/daemon/responder.mjs")));
|
|
1287
|
+
} finally {
|
|
1288
|
+
await fsp.rm(root, { recursive: true, force: true });
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
test("upgrade prune never touches merge-mode agents/", async () => {
|
|
1293
|
+
const custom = "agents/my-private-agent.md";
|
|
1294
|
+
const root = await makeAgentWithOrphans({ [custom]: "# mine\n" });
|
|
1295
|
+
try {
|
|
1296
|
+
const r = runCli(["upgrade"], root);
|
|
1297
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1298
|
+
assert.ok(
|
|
1299
|
+
existsSync(join(root, custom)),
|
|
1300
|
+
"agents/ is merge-mode: local-only files there are the point, not orphans",
|
|
1301
|
+
);
|
|
1302
|
+
} finally {
|
|
1303
|
+
await fsp.rm(root, { recursive: true, force: true });
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
test("upgrade prune never removes machine-generated launchd plists", async () => {
|
|
1308
|
+
// A committed plist is tracked, clean, and absent upstream — it satisfies
|
|
1309
|
+
// every safety check prune has, yet it is this machine's schedule, not
|
|
1310
|
+
// framework material. Regression: an early prune deleted the very plist the
|
|
1311
|
+
// cadence-bus migration was about to back up and regenerate.
|
|
1312
|
+
const plist = "scripts/local-triggers/plists/ai.adaptic.some-trigger.plist";
|
|
1313
|
+
const root = await makeAgentWithOrphans({ [plist]: "<?xml version=\"1.0\"?><plist/>" });
|
|
1314
|
+
try {
|
|
1315
|
+
const r = runCli(["upgrade"], root);
|
|
1316
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1317
|
+
assert.ok(
|
|
1318
|
+
existsSync(join(root, plist)),
|
|
1319
|
+
"generated plists are machine config; prune must leave them alone",
|
|
1320
|
+
);
|
|
1321
|
+
} finally {
|
|
1322
|
+
await fsp.rm(root, { recursive: true, force: true });
|
|
1323
|
+
}
|
|
1324
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cohortapp/agent-sdk",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.1",
|
|
4
4
|
"description": "Cohort Agent SDK \u2014 autonomous AI colleague runtime. Deploy senior AI colleagues on dedicated Mac minis, wired to the Cohort operating surface.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,21 +25,30 @@
|
|
|
25
25
|
|
|
26
26
|
import { test } from "node:test";
|
|
27
27
|
import assert from "node:assert/strict";
|
|
28
|
-
import {
|
|
28
|
+
import { readFileSync } from "node:fs";
|
|
29
29
|
|
|
30
30
|
import * as classifier from "./classifier.mjs";
|
|
31
31
|
|
|
32
32
|
// ── 1. Imports cleanly with no openai package present ───────────────────────
|
|
33
33
|
|
|
34
|
-
test("classifier.mjs imports cleanly
|
|
34
|
+
test("classifier.mjs imports cleanly and does not depend on openai", () => {
|
|
35
35
|
// The module-level import above already succeeded (this file loaded), which
|
|
36
|
-
// is the load-bearing assertion.
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
// is the load-bearing assertion.
|
|
37
|
+
//
|
|
38
|
+
// The invariant is about THIS MODULE's source, not the ambient node_modules
|
|
39
|
+
// tree. An earlier version asserted `require.resolve("openai")` throws, but
|
|
40
|
+
// that is a proxy that only holds inside the SDK's own checkout: every agent
|
|
41
|
+
// repo that pulls openai in transitively made this fail while the real
|
|
42
|
+
// invariant stayed true. A guard that goes red on every downstream machine
|
|
43
|
+
// just teaches operators to ignore red suites, so assert on the source.
|
|
44
|
+
const src = readFileSync(new URL("./classifier.mjs", import.meta.url), "utf-8");
|
|
45
|
+
const importsOpenai = /(?:^|\n)\s*import[^\n]*["']openai["']/.test(src)
|
|
46
|
+
|| /\bimport\(\s*["']openai["']\s*\)/.test(src)
|
|
47
|
+
|| /\brequire\(\s*["']openai["']\s*\)/.test(src);
|
|
48
|
+
assert.equal(
|
|
49
|
+
importsOpenai,
|
|
50
|
+
false,
|
|
51
|
+
"classifier.mjs must not import openai — a hard dependency here broke the whole module on agents that lack it"
|
|
43
52
|
);
|
|
44
53
|
assert.equal(typeof classifier.classifyItem, "function", "classifyItem is exported");
|
|
45
54
|
});
|