@cohortapp/agent-sdk 2.6.1 → 2.6.2
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 +68 -0
- package/bin/maestro.test.mjs +81 -1
- package/package.json +1 -1
package/bin/maestro.mjs
CHANGED
|
@@ -785,6 +785,43 @@ function isPruneExcluded(repoRel) {
|
|
|
785
785
|
return PRUNE_EXCLUDE_PREFIXES.some((p) => repoRel.startsWith(p));
|
|
786
786
|
}
|
|
787
787
|
|
|
788
|
+
// Record of what the framework actually delivered to THIS machine, written at
|
|
789
|
+
// the end of every non-dry upgrade. Per-machine state, hence .maestro/.
|
|
790
|
+
//
|
|
791
|
+
// Prune needs to answer "did the framework once ship this file?", and git
|
|
792
|
+
// cannot answer it. A tracked-and-clean file absent from upstream is EITHER a
|
|
793
|
+
// framework file deleted upstream OR a file the operator wrote and committed
|
|
794
|
+
// themselves — the two are byte-for-byte indistinguishable in git status.
|
|
795
|
+
// Getting that wrong destroys operator work: the first cut of this pass
|
|
796
|
+
// proposed deleting an agent's pre-commit secret-scan hook (protecting a live
|
|
797
|
+
// org credential) and its hand-authored workflows, purely because the SDK had
|
|
798
|
+
// never shipped them.
|
|
799
|
+
//
|
|
800
|
+
// So provenance is recorded explicitly. Prune considers a file only if the
|
|
801
|
+
// PREVIOUS manifest lists it — proof this machine received it from the SDK.
|
|
802
|
+
// A machine with no manifest yet prunes nothing and starts clean from the next
|
|
803
|
+
// upgrade; that is the safe direction to fail.
|
|
804
|
+
const SHIPPED_MANIFEST_REL = ".maestro/shipped-manifest.json";
|
|
805
|
+
|
|
806
|
+
function loadShippedManifest(cwd) {
|
|
807
|
+
try {
|
|
808
|
+
const raw = JSON.parse(readFileSync(join(cwd, SHIPPED_MANIFEST_REL), "utf-8"));
|
|
809
|
+
if (Array.isArray(raw?.files)) return new Set(raw.files);
|
|
810
|
+
} catch {
|
|
811
|
+
// Absent or unreadable — caller treats this as "provenance unknown".
|
|
812
|
+
}
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function writeShippedManifest(cwd, files) {
|
|
817
|
+
const dst = join(cwd, SHIPPED_MANIFEST_REL);
|
|
818
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
819
|
+
writeFileSync(
|
|
820
|
+
dst,
|
|
821
|
+
JSON.stringify({ version: 1, files: [...files].sort() }, null, 2) + "\n",
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
|
|
788
825
|
function sha256File(p) {
|
|
789
826
|
return createHash("sha256").update(readFileSync(p)).digest("hex");
|
|
790
827
|
}
|
|
@@ -1174,6 +1211,8 @@ Per-file behaviour:
|
|
|
1174
1211
|
const ignoredFiles = [];
|
|
1175
1212
|
const prunedFiles = [];
|
|
1176
1213
|
const pruneKeptFiles = [];
|
|
1214
|
+
// What upstream ships right now — becomes this machine's provenance record.
|
|
1215
|
+
const shippedNow = new Set();
|
|
1177
1216
|
|
|
1178
1217
|
for (const { path: relRoot, mode } of UPGRADE_PATHS) {
|
|
1179
1218
|
const srcRoot = join(MAESTRO_ROOT, relRoot);
|
|
@@ -1185,6 +1224,9 @@ Per-file behaviour:
|
|
|
1185
1224
|
const relFromRoot = relative(srcRoot, srcFile);
|
|
1186
1225
|
const dstFile = join(dstRoot, relFromRoot);
|
|
1187
1226
|
const repoRel = relative(cwd, dstFile).split(sep).join("/");
|
|
1227
|
+
// Recorded regardless of what we then decide to do with the file: the
|
|
1228
|
+
// manifest answers "does the framework ship this?", not "did we write it?"
|
|
1229
|
+
shippedNow.add(repoRel);
|
|
1188
1230
|
|
|
1189
1231
|
const isIgnored = matchesIgnore(repoRel, ignorePatterns);
|
|
1190
1232
|
|
|
@@ -1287,10 +1329,15 @@ Per-file behaviour:
|
|
|
1287
1329
|
//
|
|
1288
1330
|
// Removals are backed up under .maestro/backup/ exactly like a forced
|
|
1289
1331
|
// overwrite, so a prune is always reversible.
|
|
1332
|
+
const priorManifest = loadShippedManifest(cwd);
|
|
1333
|
+
|
|
1290
1334
|
if (flags.noPrune) {
|
|
1291
1335
|
log("Prune skipped (--no-prune): upstream-deleted files left in place.");
|
|
1292
1336
|
} else if (!inGit) {
|
|
1293
1337
|
warn("Prune skipped: not a git repo, so pristine framework files can't be told from yours.");
|
|
1338
|
+
} else if (!priorManifest) {
|
|
1339
|
+
log("Prune skipped: no shipped-manifest yet, so framework files can't be told from your own.");
|
|
1340
|
+
log(`Recording one now (${SHIPPED_MANIFEST_REL}); prune takes effect from the next upgrade.`);
|
|
1294
1341
|
} else {
|
|
1295
1342
|
for (const { path: relRoot, mode } of UPGRADE_PATHS) {
|
|
1296
1343
|
// merge-mode roots hold deliberately-local files — nothing to reconcile.
|
|
@@ -1313,6 +1360,15 @@ Per-file behaviour:
|
|
|
1313
1360
|
continue;
|
|
1314
1361
|
}
|
|
1315
1362
|
|
|
1363
|
+
// Provenance gate: unless a previous upgrade recorded delivering this
|
|
1364
|
+
// file, it is the operator's until proven otherwise. See the note on
|
|
1365
|
+
// SHIPPED_MANIFEST_REL — this is what stops prune eating hand-authored
|
|
1366
|
+
// scripts, workflows and policies that merely sit under a smart root.
|
|
1367
|
+
if (!priorManifest.has(repoRel)) {
|
|
1368
|
+
if (flags.verbose) console.log(` · ${repoRel} (never shipped by the framework — yours)`);
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1316
1372
|
if (matchesIgnore(repoRel, ignorePatterns)) {
|
|
1317
1373
|
counts.ignored++;
|
|
1318
1374
|
ignoredFiles.push(repoRel);
|
|
@@ -1341,6 +1397,18 @@ Per-file behaviour:
|
|
|
1341
1397
|
}
|
|
1342
1398
|
}
|
|
1343
1399
|
|
|
1400
|
+
// Record provenance for the next run. Written even when nothing was pruned —
|
|
1401
|
+
// it is what makes the FOLLOWING upgrade able to prune safely. Never written
|
|
1402
|
+
// on a dry run, so --dry-run stays genuinely side-effect-free.
|
|
1403
|
+
if (!flags.dryRun) {
|
|
1404
|
+
try {
|
|
1405
|
+
writeShippedManifest(cwd, shippedNow);
|
|
1406
|
+
} catch (err) {
|
|
1407
|
+
// Non-fatal: a missing manifest costs pruning, not correctness.
|
|
1408
|
+
warn(`Could not write ${SHIPPED_MANIFEST_REL}: ${err.message}`);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1344
1412
|
// Ensure standard runtime directories exist.
|
|
1345
1413
|
const ensureDirs = [
|
|
1346
1414
|
"state/handoffs", "state/huddle", "state/indexes", "state/rag",
|
package/bin/maestro.test.mjs
CHANGED
|
@@ -1160,7 +1160,7 @@ test("doctor surfaces the new framework systems (org / secrets / diagnostics)",
|
|
|
1160
1160
|
* Agent repo carrying `orphans` — paths upstream does not ship. Written before
|
|
1161
1161
|
* the commit so git sees them as tracked and clean (prune's precondition).
|
|
1162
1162
|
*/
|
|
1163
|
-
async function makeAgentWithOrphans(orphans) {
|
|
1163
|
+
async function makeAgentWithOrphans(orphans, opts = {}) {
|
|
1164
1164
|
const root = await tmpRoot("maestro-orphan");
|
|
1165
1165
|
mkdirSync(join(root, "config"), { recursive: true });
|
|
1166
1166
|
writeFileSync(join(root, "CLAUDE.md"), "# agent\n");
|
|
@@ -1170,6 +1170,17 @@ async function makeAgentWithOrphans(orphans) {
|
|
|
1170
1170
|
mkdirSync(dirname(join(root, rel)), { recursive: true });
|
|
1171
1171
|
writeFileSync(join(root, rel), body);
|
|
1172
1172
|
}
|
|
1173
|
+
// Provenance: prune only touches files a PREVIOUS upgrade recorded shipping.
|
|
1174
|
+
// Without this the orphans are indistinguishable from operator-authored work
|
|
1175
|
+
// and prune correctly refuses to act — so every prune test must seed it.
|
|
1176
|
+
if (opts.seedManifest !== false) {
|
|
1177
|
+
mkdirSync(join(root, ".maestro"), { recursive: true });
|
|
1178
|
+
writeFileSync(
|
|
1179
|
+
join(root, ".maestro/shipped-manifest.json"),
|
|
1180
|
+
JSON.stringify({ version: 1, files: Object.keys(orphans).sort() }, null, 2) + "\n",
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1173
1184
|
execFileSync("git", ["init", "-q"], { cwd: root });
|
|
1174
1185
|
execFileSync("git", ["add", "-A"], { cwd: root });
|
|
1175
1186
|
execFileSync(
|
|
@@ -1322,3 +1333,72 @@ test("upgrade prune never removes machine-generated launchd plists", async () =>
|
|
|
1322
1333
|
await fsp.rm(root, { recursive: true, force: true });
|
|
1323
1334
|
}
|
|
1324
1335
|
});
|
|
1336
|
+
|
|
1337
|
+
test("upgrade prune spares operator-authored files the framework never shipped", async () => {
|
|
1338
|
+
// The hazard that forced the provenance manifest. scripts/ is a smart root,
|
|
1339
|
+
// so an operator's own committed script there is tracked, clean and absent
|
|
1340
|
+
// upstream — every signal git can offer says "orphan". On a real agent
|
|
1341
|
+
// machine this set included the pre-commit hook guarding a live org
|
|
1342
|
+
// credential. Provenance, not cleanliness, is what makes prune safe.
|
|
1343
|
+
const mine = "scripts/pre-commit-secret-scan.sh";
|
|
1344
|
+
const root = await makeAgentWithOrphans({
|
|
1345
|
+
[mine]: "#!/bin/sh\nexit 0\n",
|
|
1346
|
+
[ORPHAN]: "export const gone = 1;\n",
|
|
1347
|
+
});
|
|
1348
|
+
try {
|
|
1349
|
+
// Manifest records ONLY the framework file — the operator's script was
|
|
1350
|
+
// never delivered by the SDK, so it must not be a prune candidate.
|
|
1351
|
+
writeFileSync(
|
|
1352
|
+
join(root, ".maestro/shipped-manifest.json"),
|
|
1353
|
+
JSON.stringify({ version: 1, files: [ORPHAN] }, null, 2) + "\n",
|
|
1354
|
+
);
|
|
1355
|
+
execFileSync("git", ["add", "-A"], { cwd: root });
|
|
1356
|
+
execFileSync(
|
|
1357
|
+
"git",
|
|
1358
|
+
["-c", "user.email=test@test", "-c", "user.name=test", "commit", "-q", "-m", "manifest"],
|
|
1359
|
+
{ cwd: root },
|
|
1360
|
+
);
|
|
1361
|
+
|
|
1362
|
+
const r = runCli(["upgrade"], root);
|
|
1363
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1364
|
+
assert.ok(existsSync(join(root, mine)), "a file the framework never shipped is the operator's");
|
|
1365
|
+
assert.ok(!existsSync(join(root, ORPHAN)), "the genuinely-shipped orphan should still prune");
|
|
1366
|
+
} finally {
|
|
1367
|
+
await fsp.rm(root, { recursive: true, force: true });
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
test("upgrade prunes nothing on a machine with no shipped-manifest, then records one", async () => {
|
|
1372
|
+
const root = await makeAgentWithOrphans({ [ORPHAN]: "export const gone = 1;\n" }, { seedManifest: false });
|
|
1373
|
+
try {
|
|
1374
|
+
const first = runCli(["upgrade"], root);
|
|
1375
|
+
assert.equal(first.status, 0, first.stderr);
|
|
1376
|
+
assert.ok(
|
|
1377
|
+
existsSync(join(root, ORPHAN)),
|
|
1378
|
+
"with no provenance record, prune must do nothing rather than guess",
|
|
1379
|
+
);
|
|
1380
|
+
assert.ok(
|
|
1381
|
+
existsSync(join(root, ".maestro/shipped-manifest.json")),
|
|
1382
|
+
"the first run must record provenance so the next one can prune",
|
|
1383
|
+
);
|
|
1384
|
+
const manifest = JSON.parse(readFileSync(join(root, ".maestro/shipped-manifest.json"), "utf-8"));
|
|
1385
|
+
assert.ok(manifest.files.includes("scripts/daemon/responder.mjs"), "manifest lists shipped files");
|
|
1386
|
+
assert.ok(!manifest.files.includes(ORPHAN), "manifest must not list what upstream doesn't ship");
|
|
1387
|
+
} finally {
|
|
1388
|
+
await fsp.rm(root, { recursive: true, force: true });
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
test("upgrade --dry-run writes no shipped-manifest", async () => {
|
|
1393
|
+
const root = await makeAgentWithOrphans({}, { seedManifest: false });
|
|
1394
|
+
try {
|
|
1395
|
+
const r = runCli(["upgrade", "--dry-run"], root);
|
|
1396
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1397
|
+
assert.ok(
|
|
1398
|
+
!existsSync(join(root, ".maestro/shipped-manifest.json")),
|
|
1399
|
+
"--dry-run must stay side-effect-free, including provenance",
|
|
1400
|
+
);
|
|
1401
|
+
} finally {
|
|
1402
|
+
await fsp.rm(root, { recursive: true, force: true });
|
|
1403
|
+
}
|
|
1404
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cohortapp/agent-sdk",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.2",
|
|
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": {
|