oh-my-codex 0.21.22 → 0.21.24
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/Cargo.lock +6 -6
- package/Cargo.toml +1 -1
- package/dist/cli/__tests__/plugin-cache-symlink-provenance-3552.test.js +24 -0
- package/dist/cli/__tests__/plugin-cache-symlink-provenance-3552.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +210 -2
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/team-decompose.test.js +207 -0
- package/dist/cli/__tests__/team-decompose.test.js.map +1 -1
- package/dist/cli/plugin-marketplace.d.ts +3 -0
- package/dist/cli/plugin-marketplace.d.ts.map +1 -1
- package/dist/cli/plugin-marketplace.js +103 -26
- package/dist/cli/plugin-marketplace.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +2 -4
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/team.d.ts +2 -6
- package/dist/cli/team.d.ts.map +1 -1
- package/dist/cli/team.js +44 -24
- package/dist/cli/team.js.map +1 -1
- package/dist/team/__tests__/completion-evidence.test.js +207 -0
- package/dist/team/__tests__/completion-evidence.test.js.map +1 -1
- package/dist/team/__tests__/repo-aware-decomposition.test.js +61 -0
- package/dist/team/__tests__/repo-aware-decomposition.test.js.map +1 -1
- package/dist/team/__tests__/task-evidence-intent.test.d.ts +2 -0
- package/dist/team/__tests__/task-evidence-intent.test.d.ts.map +1 -0
- package/dist/team/__tests__/task-evidence-intent.test.js +184 -0
- package/dist/team/__tests__/task-evidence-intent.test.js.map +1 -0
- package/dist/team/repo-aware-decomposition.d.ts.map +1 -1
- package/dist/team/repo-aware-decomposition.js +21 -12
- package/dist/team/repo-aware-decomposition.js.map +1 -1
- package/dist/team/task-evidence-intent.d.ts +6 -0
- package/dist/team/task-evidence-intent.d.ts.map +1 -0
- package/dist/team/task-evidence-intent.js +111 -0
- package/dist/team/task-evidence-intent.js.map +1 -0
- package/package.json +1 -1
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
|
@@ -10,12 +10,13 @@ import { decideWindowsNativeHookShimReference, setNativeHookClaimJournalDurabili
|
|
|
10
10
|
import { resolveSetupRefreshArgs } from "../update.js";
|
|
11
11
|
import { uninstall } from "../uninstall.js";
|
|
12
12
|
import { OMX_DEVELOPER_INSTRUCTIONS, OMX_PLUGIN_DEVELOPER_INSTRUCTIONS, } from "../../config/generator.js";
|
|
13
|
-
import { materializePackagedOmxPluginCache, registerOmxPluginCacheLivePin, resolvePackagedOmxMarketplace, computeOmxPluginCacheClaimDigest, } from "../plugin-marketplace.js";
|
|
13
|
+
import { materializePackagedOmxPluginCache, registerOmxPluginCacheLivePin, resolvePackagedOmxMarketplace, computeOmxPluginCacheClaimDigest, setPluginCacheMutationHooksForTest, } from "../plugin-marketplace.js";
|
|
14
14
|
import { buildManagedCodexHookTrustState, buildManagedCodexHooksConfig, buildManagedCodexNativeHookWindowsShimContent, buildManagedCodexNativeHookWindowsShimPath, buildManagedCodexNativeHookWindowsShimPathForFilesystem, } from "../../config/codex-hooks.js";
|
|
15
15
|
const packageRoot = process.cwd();
|
|
16
16
|
async function writeBoundCompletionMarker(cacheDir) {
|
|
17
17
|
const claimDigest = await computeOmxPluginCacheClaimDigest(cacheDir);
|
|
18
|
-
|
|
18
|
+
const version = basename(cacheDir);
|
|
19
|
+
await writeFile(join(cacheDir, ".omx-complete"), `${JSON.stringify({ version, claimDigest })}\n`);
|
|
19
20
|
}
|
|
20
21
|
let previousPathForFakeCodex;
|
|
21
22
|
let fakeCodexBinDir = null;
|
|
@@ -481,6 +482,15 @@ async function seedSameVersionPluginCacheWithStaleHooks(codexHomeDir) {
|
|
|
481
482
|
assert.ok(preToolUse, "expected packaged plugin PreToolUse hook fixture");
|
|
482
483
|
preToolUse.matcher = "Bash";
|
|
483
484
|
await writeFile(hooksPath, JSON.stringify(hooks, null, 2) + "\n");
|
|
485
|
+
const manifest = JSON.parse(await readFile(join(packageRoot, "plugins", "oh-my-codex", ".codex-plugin", "plugin.json"), "utf-8"));
|
|
486
|
+
const ownershipRecord = {
|
|
487
|
+
version: 1,
|
|
488
|
+
cacheVersion: manifest.version,
|
|
489
|
+
pid: 99999999,
|
|
490
|
+
processStartHash: null,
|
|
491
|
+
livePinSupportVersion: 1,
|
|
492
|
+
};
|
|
493
|
+
await writeFile(join(cacheDir, ".omx-managed"), `${JSON.stringify(ownershipRecord)}\n`);
|
|
484
494
|
await writeBoundCompletionMarker(cacheDir);
|
|
485
495
|
return cacheDir;
|
|
486
496
|
}
|
|
@@ -491,6 +501,15 @@ async function seedSameVersionPluginCacheWithStaleLauncher(codexHomeDir) {
|
|
|
491
501
|
recursive: true,
|
|
492
502
|
});
|
|
493
503
|
await writeFile(join(cacheDir, "hooks", "omx-command.json"), JSON.stringify({ command: "/stale/node", argsPrefix: ["/stale/omx.js"] }, null, 2) + "\n");
|
|
504
|
+
const manifest = JSON.parse(await readFile(join(packageRoot, "plugins", "oh-my-codex", ".codex-plugin", "plugin.json"), "utf-8"));
|
|
505
|
+
const ownershipRecord = {
|
|
506
|
+
version: 1,
|
|
507
|
+
cacheVersion: manifest.version,
|
|
508
|
+
pid: 99999999,
|
|
509
|
+
processStartHash: null,
|
|
510
|
+
livePinSupportVersion: 1,
|
|
511
|
+
};
|
|
512
|
+
await writeFile(join(cacheDir, ".omx-managed"), `${JSON.stringify(ownershipRecord)}\n`);
|
|
494
513
|
await writeBoundCompletionMarker(cacheDir);
|
|
495
514
|
return cacheDir;
|
|
496
515
|
}
|
|
@@ -1191,6 +1210,158 @@ describe("omx setup install mode behavior", () => {
|
|
|
1191
1210
|
await rm(wd, { recursive: true, force: true });
|
|
1192
1211
|
}
|
|
1193
1212
|
});
|
|
1213
|
+
it("asserts removalSafe field values for cache repair outcomes", async () => {
|
|
1214
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-removalSafe-"));
|
|
1215
|
+
try {
|
|
1216
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
1217
|
+
await withTempCwd(wd, async () => {
|
|
1218
|
+
const packagedMarketplace = await resolvePackagedOmxMarketplace(packageRoot);
|
|
1219
|
+
assert.ok(packagedMarketplace);
|
|
1220
|
+
// Test 1: untrusted/symlink cache - removalSafe should be false
|
|
1221
|
+
const cacheBaseDir = await packagedPluginCacheDir(codexHomeDir);
|
|
1222
|
+
await mkdir(dirname(cacheBaseDir), { recursive: true });
|
|
1223
|
+
const externalCacheDir = join(wd, "external-cache");
|
|
1224
|
+
await mkdir(externalCacheDir, { recursive: true });
|
|
1225
|
+
await symlink(externalCacheDir, cacheBaseDir);
|
|
1226
|
+
const untrustedResult = await materializePackagedOmxPluginCache(codexHomeDir, packagedMarketplace);
|
|
1227
|
+
assert.equal(untrustedResult.status, "stale-launcher");
|
|
1228
|
+
assert.match(untrustedResult.reason ?? "", /symlink or non-directory entry/);
|
|
1229
|
+
assert.equal(untrustedResult.removalSafe, false, "untrusted symlink should have removalSafe=false");
|
|
1230
|
+
// Test 2a: late-pin - pin created after inspection but before removal
|
|
1231
|
+
const wd2a = await mkdtemp(join(tmpdir(), "omx-setup-late-pin-"));
|
|
1232
|
+
try {
|
|
1233
|
+
await withIsolatedUserHome(wd2a, async (codexHomeDir2a) => {
|
|
1234
|
+
await withTempCwd(wd2a, async () => {
|
|
1235
|
+
// Setup: create a valid managed cache
|
|
1236
|
+
const cacheDir = await seedSameVersionPluginCacheWithStaleHooks(codexHomeDir2a);
|
|
1237
|
+
await writeBoundCompletionMarker(cacheDir);
|
|
1238
|
+
// Setup: inject a hook that creates a live pin AFTER inspection but BEFORE actual removal
|
|
1239
|
+
let hookCalled = false;
|
|
1240
|
+
const originalRestore = setPluginCacheMutationHooksForTest({
|
|
1241
|
+
beforeRemoveSyscall: async (path) => {
|
|
1242
|
+
// Simulate late-pin: create a live pin after the initial checks
|
|
1243
|
+
// Run the hook only once, the first time it's called
|
|
1244
|
+
if (!hookCalled) {
|
|
1245
|
+
hookCalled = true;
|
|
1246
|
+
const { createHash } = await import("node:crypto");
|
|
1247
|
+
const sessionHash = createHash("sha256").update("test-late-pin-session").digest("hex");
|
|
1248
|
+
const pinDir = join(path, ".omx-live-pin");
|
|
1249
|
+
await mkdir(pinDir, { recursive: true });
|
|
1250
|
+
const pinRecord = {
|
|
1251
|
+
version: 1,
|
|
1252
|
+
pid: process.pid,
|
|
1253
|
+
processStartHash: null,
|
|
1254
|
+
sessionHash,
|
|
1255
|
+
};
|
|
1256
|
+
const pinFilePath = join(pinDir, `session-${sessionHash}.json`);
|
|
1257
|
+
await writeFile(pinFilePath, `${JSON.stringify(pinRecord)}\n`);
|
|
1258
|
+
}
|
|
1259
|
+
},
|
|
1260
|
+
});
|
|
1261
|
+
try {
|
|
1262
|
+
const latePinResult = await materializePackagedOmxPluginCache(codexHomeDir2a, packagedMarketplace, { repairCache: true });
|
|
1263
|
+
assert.equal(latePinResult.status, "stale-launcher", "(a) late-pin should result in stale-launcher status");
|
|
1264
|
+
assert.equal(hookCalled, true, "(a) late-pin: pin must be created after inspection, inside the removal path");
|
|
1265
|
+
assert.match(latePinResult.reason ?? "", /pinned by a live Codex session|became pinned or changed during repair/, "(a) late-pin: should cite pin/change reason");
|
|
1266
|
+
assert.equal(latePinResult.removalSafe, false, "(a) late-pin should have removalSafe=false");
|
|
1267
|
+
// Verify snapshot remains untouched
|
|
1268
|
+
assert.equal(existsSync(cacheDir), true, "(a) late-pin: snapshot should remain untouched");
|
|
1269
|
+
}
|
|
1270
|
+
finally {
|
|
1271
|
+
originalRestore();
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
finally {
|
|
1277
|
+
await rm(wd2a, { recursive: true, force: true });
|
|
1278
|
+
}
|
|
1279
|
+
// Test 2b: live-pin present before repair
|
|
1280
|
+
const wd2b = await mkdtemp(join(tmpdir(), "omx-setup-live-pin-"));
|
|
1281
|
+
try {
|
|
1282
|
+
await withIsolatedUserHome(wd2b, async (codexHomeDir2b) => {
|
|
1283
|
+
await withTempCwd(wd2b, async () => {
|
|
1284
|
+
// Setup: create a managed cache with a stale launcher and a live pin
|
|
1285
|
+
// This ensures the test exercises the repair eligibility path (not the unchanged fast path)
|
|
1286
|
+
const cacheDir = await seedSameVersionPluginCacheWithStaleLauncher(codexHomeDir2b);
|
|
1287
|
+
await registerOmxPluginCacheLivePin(cacheDir, "test-live-pin-session", process.pid);
|
|
1288
|
+
const livePinResult = await materializePackagedOmxPluginCache(codexHomeDir2b, packagedMarketplace, { repairCache: true });
|
|
1289
|
+
assert.equal(livePinResult.status, "stale-launcher", "(b) live-pin should result in stale-launcher status");
|
|
1290
|
+
assert.match(livePinResult.reason ?? "", /pinned by a live Codex session/, "(b) live-pin: should cite live session reason");
|
|
1291
|
+
assert.equal(livePinResult.removalSafe, false, "(b) live-pin should have removalSafe=false");
|
|
1292
|
+
});
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
finally {
|
|
1296
|
+
await rm(wd2b, { recursive: true, force: true });
|
|
1297
|
+
}
|
|
1298
|
+
// Test 2c: safe repair - no pins, successful removal
|
|
1299
|
+
const wd2c = await mkdtemp(join(tmpdir(), "omx-setup-safe-repair-"));
|
|
1300
|
+
try {
|
|
1301
|
+
await withIsolatedUserHome(wd2c, async (codexHomeDir2c) => {
|
|
1302
|
+
await withTempCwd(wd2c, async () => {
|
|
1303
|
+
// Setup: create a stale cache that can be safely removed
|
|
1304
|
+
const cacheDir = await seedSameVersionPluginCacheWithStaleHooks(codexHomeDir2c);
|
|
1305
|
+
await writeBoundCompletionMarker(cacheDir);
|
|
1306
|
+
const safeRepairResult = await materializePackagedOmxPluginCache(codexHomeDir2c, packagedMarketplace, { repairCache: true });
|
|
1307
|
+
assert.equal(safeRepairResult.status, "materialized", "(c) safe repair should succeed and materialize");
|
|
1308
|
+
// removalSafe should be undefined or true (treated as safe)
|
|
1309
|
+
assert.notEqual(safeRepairResult.removalSafe, false, "(c) safe repair should NOT have removalSafe=false");
|
|
1310
|
+
});
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
finally {
|
|
1314
|
+
await rm(wd2c, { recursive: true, force: true });
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
finally {
|
|
1320
|
+
await rm(wd, { recursive: true, force: true });
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
it("does not suggest codex plugin remove for dry-run repair with live pin", async () => {
|
|
1324
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-dry-run-live-pin-"));
|
|
1325
|
+
try {
|
|
1326
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
1327
|
+
await withTempCwd(wd, async () => {
|
|
1328
|
+
await mkdir(join(wd, ".omx"), { recursive: true });
|
|
1329
|
+
const hudConfigPath = join(wd, ".omx", "hud-config.json");
|
|
1330
|
+
const userHudConfig = '{"preset":"custom"}\n';
|
|
1331
|
+
await writeFile(hudConfigPath, userHudConfig);
|
|
1332
|
+
const packagedMarketplace = await resolvePackagedOmxMarketplace(packageRoot);
|
|
1333
|
+
assert.ok(packagedMarketplace);
|
|
1334
|
+
const initial = await materializePackagedOmxPluginCache(codexHomeDir, packagedMarketplace);
|
|
1335
|
+
assert.equal(initial.status, "materialized");
|
|
1336
|
+
const cacheDir = initial.cacheDir;
|
|
1337
|
+
// Register a live pin
|
|
1338
|
+
await registerOmxPluginCacheLivePin(cacheDir, "dry-run-live-session", process.pid);
|
|
1339
|
+
// Run setup with dry-run and repair on byte-identical cache with live pin
|
|
1340
|
+
// The cache is unchanged (byte-identical), so dry-run should return unchanged, not report removal
|
|
1341
|
+
const dryRunOutput = await captureConsoleOutput(async () => {
|
|
1342
|
+
await setup({
|
|
1343
|
+
scope: "user",
|
|
1344
|
+
installMode: "plugin",
|
|
1345
|
+
repairPluginCache: true,
|
|
1346
|
+
dryRun: true,
|
|
1347
|
+
pluginAgentsMdPrompt: async () => false,
|
|
1348
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\ngoals experimental true\n",
|
|
1349
|
+
codexVersionProbe: () => "codex-cli 0.999.0",
|
|
1350
|
+
});
|
|
1351
|
+
});
|
|
1352
|
+
// Verify that removal was NOT suggested (because cache is unchanged)
|
|
1353
|
+
assert.doesNotMatch(dryRunOutput, /codex plugin remove/);
|
|
1354
|
+
// Verify that "close that session" advice is NOT printed (because cache is unchanged, no repair needed)
|
|
1355
|
+
assert.doesNotMatch(dryRunOutput, /close that session before repairing it/);
|
|
1356
|
+
// Cache should report as already correct
|
|
1357
|
+
assert.match(dryRunOutput, /Local Codex plugin cache already exposes packaged OMX skills/);
|
|
1358
|
+
});
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
finally {
|
|
1362
|
+
await rm(wd, { recursive: true, force: true });
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1194
1365
|
it("rejects scoped plugin-cache repair outside plugin setup mode", async () => {
|
|
1195
1366
|
const wd = await mkdtemp(join(tmpdir(), "omx-setup-plugin-cache-mode-"));
|
|
1196
1367
|
try {
|
|
@@ -1211,6 +1382,43 @@ describe("omx setup install mode behavior", () => {
|
|
|
1211
1382
|
await rm(wd, { recursive: true, force: true });
|
|
1212
1383
|
}
|
|
1213
1384
|
});
|
|
1385
|
+
it("verifies setup output uses removalSafe field to gate 'codex plugin remove' advice", async () => {
|
|
1386
|
+
// Test that setup.ts checks removalSafe !== false before advising 'codex plugin remove'
|
|
1387
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-removal-gate-"));
|
|
1388
|
+
try {
|
|
1389
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
1390
|
+
await withTempCwd(wd, async () => {
|
|
1391
|
+
const packagedMarketplace = await resolvePackagedOmxMarketplace(packageRoot);
|
|
1392
|
+
assert.ok(packagedMarketplace);
|
|
1393
|
+
// (d-i) Verify that removalSafe=false (untrusted symlink cache) does NOT output 'codex plugin remove' advice
|
|
1394
|
+
const cacheBaseDir = await packagedPluginCacheDir(codexHomeDir);
|
|
1395
|
+
await mkdir(dirname(cacheBaseDir), { recursive: true });
|
|
1396
|
+
const externalCacheDir = join(wd, "external-cache");
|
|
1397
|
+
await mkdir(externalCacheDir, { recursive: true });
|
|
1398
|
+
await symlink(externalCacheDir, cacheBaseDir);
|
|
1399
|
+
const unsafeOutput = await captureConsoleOutput(async () => {
|
|
1400
|
+
await setup({ scope: "user", installMode: "plugin" });
|
|
1401
|
+
});
|
|
1402
|
+
// When removalSafe=false, the setup command SHOULD NOT output 'codex plugin remove' advice
|
|
1403
|
+
assert.match(unsafeOutput, /symlink or non-directory entry/);
|
|
1404
|
+
assert.doesNotMatch(unsafeOutput, /codex plugin remove/, "(d-i) untrusted symlink cache should not suggest 'codex plugin remove'");
|
|
1405
|
+
// (d-ii) Verify that safe repair (dead launcher) DOES output 'codex plugin remove' advice
|
|
1406
|
+
await rm(codexHomeDir, { recursive: true, force: true });
|
|
1407
|
+
await mkdir(dirname(codexHomeDir), { recursive: true });
|
|
1408
|
+
const cacheDir = await seedSameVersionPluginCacheWithStaleLauncher(codexHomeDir);
|
|
1409
|
+
const safeOutput = await captureConsoleOutput(async () => {
|
|
1410
|
+
await setup({ scope: "user", installMode: "plugin" });
|
|
1411
|
+
});
|
|
1412
|
+
// When removalSafe !== false (safe repair for dead launcher), the setup command SHOULD output 'codex plugin remove' advice
|
|
1413
|
+
assert.match(safeOutput, /pinned launcher command does not exist/);
|
|
1414
|
+
assert.match(safeOutput, /codex plugin remove oh-my-codex@oh-my-codex-local --json/, "(d-ii) dead launcher safe repair should suggest 'codex plugin remove'");
|
|
1415
|
+
});
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
finally {
|
|
1419
|
+
await rm(wd, { recursive: true, force: true });
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1214
1422
|
it("refuses to replace an already published plugin cache root", async () => {
|
|
1215
1423
|
const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
|
|
1216
1424
|
try {
|